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.