Skip to content

Define a schema and generate interfaces

The schema connects local operations to typed backend inputs. It describes the records your app keeps locally; your backend's tables and business logic can have a different shape.

Define records and an operation

Create models/entry.model:

model Entry {
  id String
  text String
  note String?
  @@id(id)
}

mutation Edit {
  entry Entry.update<text,note>
}

@@id(id) defines identity. String? is nullable. Edit declares one update slot named entry; clients can change text and note but cannot change identity through that patch. It generates both the local operation and the backend's EditInput type.

Generate from a source checkout

For the existing repository example, run from the repository root:

cargo run -p ahead-compiler -- compile \
  examples/rust-round-trip/models examples/rust-round-trip/generated \
  --backend-runtime ../../../packages/server/index.mts \
  --client-runtime ../../../packages/client-js/index.mts

The runtime import paths are relative to the generated output directory. Adjust them when generating into another directory. Follow getting started to build the required native artifacts. Packages are not currently published; the default package specifiers are not a registry installation guide.

The compiler writes TypeScript and Dart clients, typed backend interfaces, descriptors and retained mutation history. See the compiler reference for every output and option. Do not edit generated files by hand.

Connect the generated layers

Generated interface Your use
client.models.entry Local get, query and watch
tx.mutate.edit Apply the local update and queue Edit
Handlers<Tx>.edit Implement authoritative business logic for Edit
Loaders<Tx>.entry Return current records from your backend
Backend Entry(identity) Identify a changed record in notify

On the client, an update slot takes { identity, values } in TypeScript. In the handler, its decoded input is { identity, patch }. Dart exposes a typed EditEntryUpdate whose fields use Present. These are generated views of the same mutation contract, not independently matched API names.

See generated client usage and backend usage for complete examples.

Group several changes into one mutation

Declare several named slots in the same mutation. For example:

model Project {
  id String
  title String
  @@id(id)
}
model Task {
  id String
  projectId String
  title String
  @@id(id)
}
mutation CreateProject {
  project Project.create
  tasks Task.create[]
}

CreateProject becomes one typed client call and one backend handler. Its declared operations apply together locally, and its backend business writes share one mutation savepoint. The list slot supplies zero or more complete task records. If you need a declared relationship as well, add a reference; a field named projectId alone does not create one automatically.

Several tx.mutate calls in a local transaction commit locally together, but remain separate backend mutations with separate rejection outcomes. Choose one multi-slot mutation when the business operation must be accepted or rejected as one unit.

Relations, prerequisites and ordering

Declare a forward reference with @reference(via: [field]) and an inverse with the related model type. Relations generate local navigation methods and let the runtime enforce the declared contract. The relations fixture shows Book.comments and Comment.book.

Prerequisites declare host work that must finish before sending a mutation. For example, a schema can declare prerequisite Uploaded(key String) and use @requires(Uploaded(key: self)) on a field. Your application provides the async callback through runPrerequisites. Prerequisites are not automatically implemented uploads.

Slot bindings and @@sequence express operation dependencies. Use the compiler's tested declarations as syntax examples for these advanced features; they affect scheduling, not just generated types.

Evolve the contract

Keep mutation-history.json with the generated artifacts. Regenerating retains prior input contracts so each queued mutation retains a defined input contract.

A compatible change can keep the same version; breaking slot/input/policy changes require @@version(n) with a newer version. Implement every supported handler version exposed by the generated backend interface. Do not delete history to silence a compatibility error.

There are three separate responsibilities: compiler compatibility checks, migration of the client's local cache, and migration of your backend database. A successful compile does not perform the other two. Read compiler compatibility and client migration before shipping a schema change.