Migrations
Structural schema evolution workflow and reviewed migration edges.
You can skip this first
If you are trying to get your first app running, you can skip this page and return later.
Why Jazz migrations are different
Traditional migration systems run a linear sequence and require peers to converge before older versions stop writing. Jazz migrations are applied at read/write time, so mixed-version clients can keep operating while data is translated between schema versions.
Workflow
-
If this is the first migration you are creating, run:
pnpm dlx jazz-tools@alpha migrations createThis creates an initial snapshot of your schema in
migrations/snapshots/. No migration file is created yet because there is no previous schema to diff against. -
Edit
schema.ts— change the data shape as needed. -
Validate locally — optionally run
pnpm dlx jazz-tools@alpha validateto ensure policies are valid before they are deployed.deploydoes not run these checks. -
Create a migration stub for the updated schema — run:
pnpm dlx jazz-tools@alpha migrations create --name <your-migration-name>By default, Jazz diffs the latest committed snapshot in
migrations/snapshots/against the current schema and writes a stub migration file intomigrations/. It also saves a snapshot of the generated schema. -
Review and customise — the migration, if needed (see below).
-
Publish — push the migration to the server:
pnpm dlx jazz-tools@alpha migrations push <appId> <fromHash> <toHash>If you also want to publish the current schema, the migration and permissions in one step, you can run:
pnpm dlx jazz-tools@alpha deploy <appId>deploypublishes the current schema if the server does not already know it, checks whether the previous permissions schema is connected to the new one on the server, pushes the local migration if needed, and then publishes the current permissions.
Permission-only changes in permissions.ts do not need migrations, but they do still require
pnpm dlx jazz-tools@alpha deploy <appId>. See Permissions for details.
Generated stub
Under the hood, each migration produces a lens — a bidirectional transformation between two schema versions. The forward direction applies the migration; the backward direction is generated automatically so older clients can still read data written under the new schema. The generated stub contains the structural diff as declarative operations.
If the diff contains ambiguities (e.g. a column was removed and a same-typed column was added, which could be a rename), the generated lens is marked as a draft. Draft lenses will fail at startup if they are in the path to a live schema. You need to review the draft lens and resolve the ambiguity before publishing.
Here's a generated stub for adding a description column:
import { schema as s } from "jazz-tools";
export default s.defineMigration({
migrate: {
todos: {
description: s.add.string({ default: null }),
},
},
fromHash: "a01f5c72ec47",
toHash: "311995e9a178",
from: {
todos: s.table({
title: s.string(),
done: s.boolean(),
parentId: s.ref("todos").optional(),
projectId: s.ref("projects").optional(),
owner_id: s.string(),
}),
},
to: {
todos: s.table({
title: s.string(),
done: s.boolean(),
description: s.string().optional(),
parentId: s.ref("todos").optional(),
projectId: s.ref("projects").optional(),
owner_id: s.string(),
}),
},
});Customising defaults
Review generated defaults before you publish. For example, you might replace a nullable default with a domain-specific value:
import { schema as s } from "jazz-tools";
// Example of editing a generated migration stub.
export default s.defineMigration({
migrate: {
todos: {
description: s.add.string({ default: "No description" }),
},
},
fromHash: "a01f5c72ec47",
toHash: "311995e9a178",
from: {
todos: s.table({
title: s.string(),
done: s.boolean(),
parentId: s.ref("todos").optional(),
projectId: s.ref("projects").optional(),
owner_id: s.string(),
}),
},
to: {
todos: s.table({
title: s.string(),
done: s.boolean(),
description: s.string().optional(),
parentId: s.ref("todos").optional(),
projectId: s.ref("projects").optional(),
owner_id: s.string(),
}),
},
});Backwards defaults
When a newer schema drops a column that older clients still expect, define a backwards default so the lens can supply a value for those clients:
import { schema as s } from "jazz-tools";
// Example: dropping a column with a backwards default.
// Clients still on the older schema continue seeing legacy_priority.
export default s.defineMigration({
migrate: {
todos: {
legacy_priority: s.drop.int({ backwardsDefault: 0 }),
},
},
fromHash: "311995e9a178",
toHash: "73b65d082ab8",
from: {
todos: s.table({
title: s.string(),
done: s.boolean(),
description: s.string().optional(),
parentId: s.ref("todos").optional(),
projectId: s.ref("projects").optional(),
owner_id: s.string(),
legacy_priority: s.int(),
}),
},
to: {
todos: s.table({
title: s.string(),
done: s.boolean(),
description: s.string().optional(),
parentId: s.ref("todos").optional(),
projectId: s.ref("projects").optional(),
owner_id: s.string(),
}),
},
});Migrating historical schemas
Jazz does not require you to create a migration for every schema change. You can just use the app and create new data. Existing data will still be available, but you will not be able to read it until you create a migration.
This is particularly useful when you are iterating on a feature that is not yet ready to be released.
The Jazz server will detect when there are rows that are not reachable from the current schema. It will log a warning and suggest you create a migration.
In order to do so, you'll need to create the migration using explicit to/from schema hashes:
pnpm dlx jazz-tools@alpha migrations create <appId> --fromHash <fromHash>
pnpm dlx jazz-tools@alpha migrations create <appId> --fromHash <fromHash> --toHash <toHash>--toHash defaults to the current local schema. When a requested hash is not already saved locally, Jazz resolves it from the
server and saves a snapshot in migrations/snapshots/.
Exporting the compiled schema
pnpm dlx jazz-tools@alpha schema export prints the compiled structural schema as JSON to stdout. It
also saves a snapshot of the schema in the local snapshot directory.
pnpm dlx jazz-tools@alpha schema export
pnpm dlx jazz-tools@alpha schema export --schema-dir ./packages/app
pnpm dlx jazz-tools@alpha schema export <appId> --schema-hash <hash> --server-url http://localhost:4200 --admin-secret <secret>Without --schema-hash, Jazz exports the current local schema.ts. With --schema-hash, it
loads the schema from the local snapshot folder or, if missing, from the server.
--schema-dir and --schema-hash are mutually exclusive.
Server-backed commands require the app id so Jazz can resolve app-scoped routes like
/apps/<appId>/schema/:hash.
| Flag | Default | Description |
|---|---|---|
--schema-dir <path> | current directory | Path to app root containing schema.ts |
--schema-hash <hash> | none | Export a stored structural schema by hash |
--migrations-dir <p> | ./migrations | Path to migrations directory and snapshot folder |
--server-url <url> | JAZZ_SERVER_URL | Server URL used when --schema-hash is not available locally |
--admin-secret <sec> | JAZZ_ADMIN_SECRET | Admin secret used when --schema-hash is not available locally |
Migration flags
migrations create uses flags rather than positional hashes. When it needs to resolve missing
schema hashes from a server, pass <appId> as the leading positional argument.
| Flag | Default | Description |
|---|---|---|
--schema-dir <path> | current directory | Path to app root containing schema.ts |
--migrations-dir <p> | ./migrations | Path to migrations directory and snapshot folder |
--server-url <url> | JAZZ_SERVER_URL | Server URL used when resolving missing schema |
--admin-secret <sec> | JAZZ_ADMIN_SECRET | Admin secret used when resolving missing schema |
--fromHash <hash> | latest snapshot | Optional source schema hash |
--toHash <hash> | current schema | Optional target schema hash |
--name <name> | unnamed | Optional migration filename label |
Next steps
- Defining Tables — table and column definitions
- Column Types — full list of available column types