.morph DSL Reference

The .morph DSL is the primary way to define domain schemas. This is the complete syntax reference.

File Structure

A .morph file contains:

  1. A domain declaration (required, exactly one)
  2. An optional extensions block
  3. An optional profiles block
  4. One or more context blocks
domain MyApp

extensions { ... }

profiles { ... }

context orders "Order management" { ... }
context inventory "Inventory tracking" { ... }

Domain

domain <Name>

Names the project. Used for package naming (e.g., @myapp/orders-core).

Extensions

extensions {
  storage [memory, jsonfile, sqlite, redis, eventsourced] default memory
  auth [none, jwt, session, apikey, password] default jwt
  eventStore [memory, jsonfile, redis] default memory
  i18n [en, de, fr] base en
  sse [true, false, auto] default auto
}

Each line declares an extension type with available options and a default. The base keyword is used for i18n’s base language. The eventsourced storage backend derives entity state from the event store (see Domain Events). The sse extension enables Server-Sent Events for real-time event streaming to HTTP clients; auto (the default) enables SSE only when the schema has commands that emit events and subscribers to handle them.

Profiles

Profiles define named groups of tags, allowing operations to target multiple app targets with a single reference.

profiles {
  web: @cli @api @mcp
  full: @cli @api @mcp @ui @vscode
}

Each entry maps a profile name to one or more literal tags. Operations reference profiles with the # prefix:

#web
command createTodo "Create a new todo."
  writes Todo
  input { title: string }
  output Todo

#web expands to @cli @api @mcp during compilation. Generators receive the expanded @ tags — no downstream changes needed.

  • #name is always a profile reference
  • @name is always a literal tag
  • Profiles and literal tags can be mixed: #web @ui

Context

context <name> "description" {
  depends on OtherContext, AnotherContext

  // entities, operations, invariants, subscribers, etc.
}

Groups related domain concepts. depends on declares cross-context dependencies.

Entities

@root
entity Todo "A task to be completed." {
  title: string "The task title"
  completed: boolean "Whether the task is done"
  @sensitive
  apiKey?: string "Optional API key"

  belongs_to User "The owner"
  has_many Comment "Comments on this todo"
}
  • @root — marks as aggregate root (required for at least one entity per context)
  • ? after attribute name — marks as optional
  • Descriptions (quoted strings) are optional on all declarations

Relationships

belongs_to <Entity> "description"
has_many <Entity> "description"
has_one <Entity> "description"
references <Entity> "description"

Attribute Tags

  • @sensitive — marks field as sensitive (excluded from logs, API responses)
  • @unique — unique constraint

Value Objects

value DueDate "A timezone-aware due date." {
  date: string "ISO date"
  timezone: string "IANA timezone"
}

Like entities but without identity or relationships. Used for complex attributes.

Commands

Commands mutate state:

@cli @api @mcp
command createTodo "Create a new todo."
  writes Todo
  pre UserExists
  input {
    userId: User.id "The owner"
    title: string "The task title"
    dueDate?: DueDate "Optional due date"
  }
  output Todo
  emits TodoCreated "Emitted on creation"
  errors {
    UserNotFound "User does not exist" when "userId is invalid"
  }

Clauses

All clauses are optional and can appear in any order:

Clause Syntax Purpose
reads reads Entity1, Entity2 Entities read by this operation
writes writes Entity1 Entities written by this operation
pre pre Invariant1, Invariant2 Preconditions checked before execution
post post Condition1 Postconditions checked after execution
input input { ... } Input parameters
output output Type Return type
emits emits Event1 "desc", Event2 Domain events emitted
errors errors { ... } Possible error conditions

Tags

Tags control which app targets expose the operation:

Tag Target
@api REST API routes
@cli CLI commands
@cli_client Client CLI (talks to API)
@mcp MCP server tools
@ui Web UI pages
@vscode VS Code extension commands

Queries

Queries read state (same syntax as commands, minus emits):

@cli @api
query listTodos "List todos for a user."
  reads Todo
  input {
    userId: User.id "Filter by owner"
    includeCompleted?: boolean "Include completed todos"
  }
  output Todo[]

Functions

Standalone functions without aggregate access:

function validateEmail "Check email format."
  input { email: string }
  output boolean
  errors { InvalidFormat "Email is malformed" }

Invariants

Business rules that must hold:

invariant TodoBelongsToUser on Todo
  "A todo must belong to an existing user."
  violation "Todo does not belong to the specified user"
  where todo.userId == currentUser.id

@context
invariant UserIdMatchesCurrentUser
  "The userId in input must match the authenticated user."
  violation "Cannot create todos for other users"
  where input.userId == context.currentUser.id
  • on Entity — scopes the invariant to an entity
  • @context — operates on request context (auth, input) rather than entity state
  • violation — error message when invariant is violated
  • where — boolean condition expression

Condition Expressions

where x == y                    // equality
where x != y                    // inequality
where x > 0 && y < 100         // logical AND with comparisons
where x || y                   // logical OR
where !deleted                 // negation
where list contains item       // membership
where exists x in items: x > 0 // existential
where forall x in items: x > 0 // universal
where if active then count > 0 // conditional

Subscribers

React to domain events:

subscriber logTodoEvents "Log events for auditing"
  on TodoCreated, TodoCompleted, TodoDeleted

Ports

Define abstract interfaces for infrastructure:

port StorageTransport<T> "Generic key-value storage" {
  get(key: string): T throws NotFoundError
  put(key: string, value: T): void
  remove(key: string): void throws NotFoundError
  getAll(): T[]
}

Type Parameters

port Container<T, U: string, V = number> "Example" {
  process(input: T): U
}
  • T — unbounded type parameter
  • U: string — constrained to string
  • V = number — default type

Method Syntax

methodName(param: Type, other: Type): ReturnType throws Error1, Error2

Contracts

Property-based tests for ports:

contract PutGetRoundTrip on StorageTransport "Put then get returns same value"
  given key: string, value: T
  when put(key, value), get(key)
  then result == value

contract RemoveThenGetFails on StorageTransport "Remove then get throws"
  given key: string, value: T
  when put(key, value), remove(key), get(key)
  then throws NotFoundError

Types

Product types (structs):

type Address "A mailing address" {
  street: string
  city: string
  zip: string
  country: string
}

Unions

Tagged unions (sum types):

union PaymentMethod by kind "How a customer pays" {
  creditCard "Pay by card" {
    last4: string
    expiry: string
  }
  bankTransfer "Pay by bank" {
    iban: string
  }
  cash
}
  • by kind — specifies the discriminator field name

Aliases

Type aliases:

alias Email = string
alias Optional<T> = T?
alias IdMap<K: string, V> = Map<K, V>

Errors

Standalone error definitions:

error ValidationError "Input validation failed" {
  field: string "The invalid field"
  message: string "What went wrong"
}

Type Expressions

Syntax Example Description
Primitive string, integer, float, boolean, date, datetime, void Built-in types
Entity ref Todo, User Entity type
ID ref Todo.id, User.id Entity’s branded ID type
Array Todo[], string[] Array of type
Optional string?, Todo? Optional type
Literal union "active" | "inactive" String literal union
Generic Map<string, number> Parameterized type
Function (x: string) => boolean Function type
Qualified OtherContext.Type Cross-context type reference

Comments

// This is a comment
domain MyApp // Inline comments work too

Only single-line comments (//) are supported.