Skip to content

Tutorial 1: Getting Started with USL

Duration: 30 minutes
Prerequisites: Basic programming knowledge
What you'll build: A simple task management application with user authentication

Overview

In this tutorial, you'll learn the fundamentals of USL by building a working task manager. You'll:

  • Define domain entities (User, Task)
  • Create authorization policies
  • Implement state transitions
  • Generate production-ready code

Installation

First, install the USL compiler:

curl -fsSL https://raw.githubusercontent.com/shaifulshabuj/USL/main/scripts/install.sh | sh
irm https://raw.githubusercontent.com/shaifulshabuj/USL/main/scripts/install.ps1 | iex

Verify installation:

usl --version
# Expected output: usl 1.0.0

Project Setup

Create a new USL project:

mkdir task-manager
cd task-manager
usl init

This creates:

task-manager/
├── task-manager.usl    # Main specification
├── usl.toml            # Project configuration
└── .gitignore

Step 1: Define Domain Entities

Open task-manager.usl and define your domain model:

domain TaskManager {
  // User entity
  entity User {
    id: UserId @primary
    email: Email @unique
    name: String
    createdAt: Timestamp
  }

  // Task entity
  entity Task {
    id: TaskId @primary
    title: String
    description: String
    ownerId: UserId
    status: TaskStatus
    createdAt: Timestamp

    // Business rule: completed tasks must have a title
    invariant completed_has_title {
      this.status == TaskStatus.Completed -> len(this.title) > 0
    }
  }

  // Task status enumeration
  enum TaskStatus {
    Todo
    InProgress
    Completed
  }
}

Key Concepts

  • Entities: Core domain objects with identity
  • @primary: Marks the unique identifier field
  • @unique: Enforces uniqueness constraint
  • Invariants: Business rules that must always hold

Type Safety

USL's type system catches errors at compile time. UserId, TaskId, and Email are custom types that prevent mixing different ID types.

Step 2: Create Authorization Policies

Add access control rules below your domain definition:

policy TaskPolicy {
  actor user: User

  // Anyone can create their own tasks
  rule create_task {
    true  // No restrictions on task creation
  }

  // Only task owners can edit their tasks
  rule edit_task(task: Task) {
    user.id == task.ownerId
  }

  // Only task owners can delete their tasks
  rule delete_task(task: Task) {
    user.id == task.ownerId
  }

  // Everyone can view tasks (for now)
  rule view_task(task: Task) {
    true
  }
}

Policy Structure

  • actor: The user performing the action
  • rule: A named authorization check
  • Parameters: Rules can reference entities to make decisions

Totality Checking

USL ensures all service actions are covered by policy rules. Missing rules cause compilation errors.

Step 3: Model Task Lifecycle

Define how tasks transition between states:

behavior TaskLifecycle for Task {
  initial state Todo

  state Todo {
    on start -> InProgress
      effects {
        this.status = TaskStatus.InProgress
      }
  }

  state InProgress {
    on complete -> Completed
      requires len(this.title) > 0
      effects {
        this.status = TaskStatus.Completed
      }

    on abandon -> Todo
      effects {
        this.status = TaskStatus.Todo
      }
  }

  state Completed {
    on reopen -> Todo
      effects {
        this.status = TaskStatus.Todo
      }
  }
}

Behavior Features

  • initial state: Where new tasks begin
  • transitions: Allowed state changes
  • requires: Preconditions (guards)
  • effects: State modifications

Formal Verification

USL proves that all transitions preserve invariants. If completed_has_title could be violated, compilation fails.

Step 4: Define Service API

Create the external API for your application:

service TaskService {
  action createTask(title: String, description: String) -> Task
    enforces TaskPolicy.create_task
    effects { Write(Task) }
    implementation {
      let task = Task {
        id: generateId(),
        title: title,
        description: description,
        ownerId: context.user.id,
        status: TaskStatus.Todo,
        createdAt: now()
      }
      store(task)
      return task
    }

  action updateTask(taskId: TaskId, newTitle: String, newDescription: String) -> Task
    enforces TaskPolicy.edit_task(task)
    effects { Write(Task) }
    implementation {
      let task = load(Task, taskId)
      task.title = newTitle
      task.description = newDescription
      store(task)
      return task
    }

  action deleteTask(taskId: TaskId) -> Void
    enforces TaskPolicy.delete_task(task)
    effects { Delete(Task) }
    implementation {
      let task = load(Task, taskId)
      delete(task)
    }

  action getTask(taskId: TaskId) -> Task
    enforces TaskPolicy.view_task(task)
    effects { Read(Task) }
    implementation {
      return load(Task, taskId)
    }

  action listTasks() -> List[Task]
    enforces TaskPolicy.view_task(task)
    effects { Read(Task) }
    implementation {
      return loadAll(Task)
    }
}

Service Components

  • action: External operation
  • enforces: Policy rule to check
  • effects: Side effects declaration
  • implementation: Business logic

Step 5: Compile and Generate Code

Compile your USL specification:

usl compile task-manager.usl

Generate TypeScript backend code:

usl generate --target typescript --output ./generated

This creates:

generated/
├── entities/
│   ├── User.ts
│   └── Task.ts
├── policies/
│   └── TaskPolicy.ts
├── services/
│   └── TaskService.ts
└── api/
    └── openapi.yaml

Step 6: Run Your Application

Install dependencies:

cd generated
npm install

Start the development server:

npm run dev

Your API is now running at http://localhost:3000!

Test the API

Create a task:

curl -X POST http://localhost:3000/api/tasks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "title": "Learn USL",
    "description": "Complete the getting started tutorial"
  }'

List tasks:

curl http://localhost:3000/api/tasks \
  -H "Authorization: Bearer YOUR_TOKEN"

What You've Learned

Domain modeling with entities and invariants
Policy-driven security with compile-time checks
State machine behavior with formal verification
Service layer design with effect tracking
Code generation for production deployment

Next Steps

Continue to Tutorial 2: Domain Modeling to learn:

  • Advanced type system features
  • Complex invariants
  • Relationships between entities
  • Computed fields and validations

Troubleshooting

Compilation Errors

If you see USL-REF-001: Undefined entity:

  • Check spelling of entity names
  • Ensure entities are defined before use

Policy Errors

If you see USL-POL-008: Policy not total:

  • Add rules for all service actions
  • Use deny { false } to explicitly deny actions

Generation Errors

If code generation fails:

  • Verify usl.toml has correct target configuration
  • Check write permissions in output directory

Additional Resources