Generating a migration
Turn a contract change into a reviewable migration with the migration plan command.
This tutorial takes a contract change from your editor to a planned migration on disk. Planning is fully offline. migration plan reads your emitted contract and your existing migrations; it never connects to a database. This means you can plan on a plane, in CI, or in a sandbox with no credentials.
Start from a minimal project; you can scaffold one with npm create prisma@latest.
Your first migration
Say your contract has a single model:
model User {
id Int @id
email String
name String?
@@map("user")
}First, emit the contract. This compiles the schema into the contract.json artifact that every other command reads:
bunx prisma@latest contract emitNow plan. --name sets the human-readable part of the directory name. If you omit it, the directory will be called migration:
bunx prisma@latest migration plan --name init✔ Planned 2 operation(s)
│
├─ Create schema "public"
└─ Create table "user"
from: (baseline)
to: 705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5
App space → migrations/app/20260707T1005_init
Next: review migrations/app/20260707T1005_init if needed, then run prisma db migrate.
DDL preview
CREATE SCHEMA IF NOT EXISTS "public";
CREATE TABLE "public"."user" (
"email" text NOT NULL,
"id" int4 NOT NULL,
"name" text,
PRIMARY KEY ("id")
);Four things to note:
- The DDL preview is in the output. You will see the exact SQL before anything exists but files.
from: (baseline)means this migration starts from an empty database. It's the root of your migration graph.to:is your contract's hash. The migration promises to deliver a database matching exactly the contract that you have emitted.App spaceis your application's migration lane. Database extensions bring their own lanes. See extension spaces for more information.
The planned migration directory contains the TypeScript source, the compiled operations, and the history marker. The contract snapshots it imports live once per migrations root, under migrations/snapshots/<contract hash>/:
migrations/
├── app/
│ └── 20260707T1005_init/
│ ├── migration.ts
│ ├── ops.json
│ └── migration.json
└── snapshots/
└── 705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5/
├── contract.json
└── contract.d.tsOpen migration.ts. It is a description of the change, so it reads like one:
import type { Contract as End } from '../../snapshots/705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5/contract';
import endContract from '../../snapshots/705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5/contract.json' with {
type: 'json',
};
import { Migration, MigrationCLI, col, primaryKey } from '@prisma/orm-postgres/migration';
export default class M extends Migration<never, End> {
override readonly endContractJson = endContract;
override get operations() {
return [
this.createSchema({ schema: 'public' }),
this.createTable({
schema: 'public',
table: 'user',
columns: [
col('email', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }),
col('id', 'int4', { notNull: true, codecRef: { codecId: 'pg/int4@1' } }),
col('name', 'text', { codecRef: { codecId: 'pg/text@1' } }),
],
constraints: [primaryKey(['id'])],
}),
];
}
}
MigrationCLI.run(import.meta.url, M);When reading it, note two things: the col(...) calls mirror your contract fields, and you never write codecRef by hand. The planner fills it in to pin how values convert between TypeScript and Postgres. Treat it as noise when reviewing.
For a simple change you don't need to touch this file. When you do (to add a data backfill, reorder operations, or drop in raw SQL), refer to Editing a migration.
The second migration: planning a delta
Apply the first migration (covered properly in Applying a migration), then make another change: add a phone field:
model User {
id Int @id
email String
name String?
phone String?
@@map("user")
}Emit and plan again. This time, tell the planner where to start from, so it plans the delta rather than the whole schema:
bunx prisma@latest contract emit
bunx prisma@latest migration plan --name add_user_phone --from 20260707T1005_init✔ Planned 1 operation(s)
│
└─ Add column "phone" to "user"
from: 705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5
to: 925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72
App space → migrations/app/20260707T1006_add_user_phone
DDL preview
ALTER TABLE "public"."user" ADD COLUMN "phone" text;--from accepts a migration directory name (as here), a contract hash or unambiguous prefix, a ref name, or <dir>^ meaning "the state before that migration".
The db ref: skipping --from
You don't have to pass --from every time. If a ref named db exists, planning starts from whatever it points at by default. Four things move it forward:
db initanddb updateadvance it after a successful run, but only when you leave--dboff and let the connection come fromprisma.config.ts. Passing--dbsuppresses the advancement unless you also pass--advance-ref db.db signadvances it after a successful signature, with or without--db. This is what makes the first plan after adopting an existing database contain only your change. Pass--no-advance-refto sign without moving it.db migrate --advance-ref dbadvances it after an apply. Plaindb migratenever advances anything; that is deliberate, so a deploy or CI apply cannot move a repository ref.migration ref set db <contract>sets it by hand.
bunx prisma@latest db update # advances db on its own
bunx prisma@latest db update --db "$DATABASE_URL" --advance-ref db # with --db, ask for it
bunx prisma@latest db sign --db "$DATABASE_URL" # advances db on its own
bunx prisma@latest db migrate --advance-ref db
bunx prisma@latest migration plan --name next_change # starts from the db ref automatically--from or a db ref, planning stopsIf there's no db ref and you omit --from, migration plan refuses with MIGRATION.PLAN_ORIGIN_UNKNOWN whenever migrations already exist on disk. Planning from empty there would produce a full CREATE-everything package that no database carrying the earlier migrations could apply, so the command stops instead and names the three exits: migration ref set db <contract>, --from <ref>, or --from @empty to plan from an empty database deliberately.
Only an empty migrations/app/ still plans from empty on its own. That is the first migration in a new project, described above. The command prints No db ref set — planning from an empty database under its summary when that happens, so you can tell a fresh first plan from a plan that recreates a database you already have.
The automatic baseline
When migrations/app/ is empty but the db ref already points at a contract whose snapshot is stored under migrations/snapshots/ — the usual state after a few db update cycles — one migration plan run writes a baseline package from nothing to the ref's contract and, when your emitted contract differs from the ref's, a second package with the delta. Expect one or two new directories in git status.
The baseline never runs against a database that already carries a marker: the runner starts at the marker and applies only the edges past it. It exists so the ref's contract is a node in the graph and the delta has somewhere to attach.
To retrofit a deployed database you must not touch, do the same thing explicitly: bring the contract source back to the deployed state, contract emit, migration plan --name baseline --from @empty, then restore your current contract and plan the delta from the baseline.
When the planner needs your input
Some changes can't be planned from the schema diff alone. Add a required field to a table that already has rows; the planner can write the ADD COLUMN and the SET NOT NULL, but only you know what the existing rows should contain. Rather than guessing, it scaffolds the decision as a placeholder:
⚠ Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit
Open migration.ts and replace each `placeholder(...)` call with your actual query.
Then run: node migrations/app/20260707T1008_add_display_name/migration.tsThe generated migration sandwiches a dataTransform between the schema steps, with placeholder(...) where your backfill query goes. Filling it in is explained in Editing a migration.
Reviewing what you planned
Three offline commands close the loop before anything runs:
# One migration in detail: operations, metadata, DDL preview
bunx prisma@latest migration show 20260707T1006_add_user_phone
# The whole graph, with your new edge in place
bunx prisma@latest migration graph
# Integrity check: hashes match, files complete, graph well-formed; exits non-zero on failure
bunx prisma@latest migration checkmigration check is designed for CI: exit code 0 means clean, 2 means it couldn't resolve what you asked for, 4 means an integrity failure, for example someone hand-edited ops.json without re-running migration.ts.
Planning covers tables, columns, indexes, constraints, and the backfill scaffold shown above. Rename inference is not built yet: renaming a field plans as drop column + add column, flagged destructive with a data-loss warning. For a true rename, edit the migration and replace the pair with a rawSql ALTER TABLE ... RENAME COLUMN. There is also no interactive mode. The planner writes its best answer and leaves refinement to you in migration.ts.
Prompt your coding agent
Projects scaffolded with create-prisma@latest install Prisma ORM skills for your coding agent. Ask your agent to:
- "Add a required
displayNamefield to User, emit the contract, and plan the migration." - "Plan a migration named
add-orders-tableand show me its DDL preview before I commit it." - "Run
migration checkand explain any integrity failures."
See also
- Editing a migration: fill placeholders, add data steps, write raw SQL
- Applying a migration: run what you planned
- Studio with Prisma ORM: once applied, see the same operations as a visual diff in Prisma Studio
- TypeScript Migrations in Prisma 8: the design story behind
migration.ts
The migration graph
You changed your schema, a teammate changed theirs, both merged. The migration graph is what lets every database catch up without renaming files or rebuilding history.
Editing a migration
A migration is TypeScript you own. Fill in backfills, reorder steps, or drop to raw SQL, then recompile it with one command.
