Schemas

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

  1. If this is the first migration you are creating, run:

    pnpm dlx jazz-tools@alpha migrations create

    This 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.

  2. Edit schema.ts — change the data shape as needed.

  3. Validate locally — optionally run pnpm dlx jazz-tools@alpha validate to ensure policies are valid before they are deployed. deploy does not run these checks.

  4. 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 into migrations/. It also saves a snapshot of the generated schema.

  5. Review and customise — the migration, if needed (see below).

  6. 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>

    deploy publishes 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:

migrations/20260318-unnamed-a01f5c72ec47-311995e9a178.ts
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:

migrations/20260318-add-description-a01f5c72ec47-311995e9a178.ts
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:

migrations/20260318-drop-legacy-priority-311995e9a178-73b65d082ab8.ts
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.

FlagDefaultDescription
--schema-dir <path>current directoryPath to app root containing schema.ts
--schema-hash <hash>noneExport a stored structural schema by hash
--migrations-dir <p>./migrationsPath to migrations directory and snapshot folder
--server-url <url>JAZZ_SERVER_URLServer URL used when --schema-hash is not available locally
--admin-secret <sec>JAZZ_ADMIN_SECRETAdmin 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.

FlagDefaultDescription
--schema-dir <path>current directoryPath to app root containing schema.ts
--migrations-dir <p>./migrationsPath to migrations directory and snapshot folder
--server-url <url>JAZZ_SERVER_URLServer URL used when resolving missing schema
--admin-secret <sec>JAZZ_ADMIN_SECRETAdmin secret used when resolving missing schema
--fromHash <hash>latest snapshotOptional source schema hash
--toHash <hash>current schemaOptional target schema hash
--name <name>unnamedOptional migration filename label

Next steps

On this page