Generating a migration
This tutorial takes a change you have made to your contract, the contract.prisma file that replaced schema.prisma, and turns it into a migration that you read and approve before it runs against a database. To create a project first, see the quickstart.
Two commands do the work, and you run them in this order every time you change your contract. npx prisma contract emit replaces prisma generate: it reads contract.prisma and writes contract.json and contract.d.ts next to it, so that everything downstream reads your latest edit and not the version before it. npx prisma migration plan then reads contract.json and writes a new migration for the change, without applying it. Planning never connects to a database, which means you can run it in CI or in a sandbox with no database credentials. To work out what your change is a change from, migration plan looks by default at the db ref, a file in migrations/app/refs/ that records the contract version you last applied in development.
Say your contract has a single model:
// use prisma-8
model User {
id Int @id
email String
name String?
@@map("user")
}Keep // use prisma-8 as the first line of contract.prisma. If you split your contract across several files, every one of them needs that line too. contract emit and the Prisma editor extension read only the .prisma files that start with it, and skip the others without a warning. If no file starts with it, contract emit fails with an error whose code is CONTRACT.SOURCE_LOAD_FAILED.
Run both commands, passing --name init so that the new migration directory is named <timestamp>_init:
bunx prisma contract emit
bunx prisma migration plan --name initpnpm prisma contract emit
pnpm prisma migration plan --name inityarn prisma contract emit
yarn prisma migration plan --name initnpx prisma contract emit
npx prisma migration plan --name init│ contract: src/prisma/contract.json
│ migrations: migrations/app
│ name: init
✔ Planned 2 operation(s)
ℹ No db ref set — planning from an empty database. Run db init, db update, or db sign if a database already exists.
migrations/app/20260707T1005_init
├─ Create schema "public"
└─ Create table "user"
from: (baseline)
to: 2d2cec8641643159cfc94e499e5eacbd75218e0389d938139b278f68618d6b13
app space: migrations/app/20260707T1005_init
ℹ 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")
);Read the rest of that output closely, because it tells you what was planned and where it was written:
Planned 2 operation(s)counts the steps in the migration, which are listed underneath the directory name, and the DDL preview below them is the SQL those steps run.to:is the hash of your contract. Each version of your contract is a contract state, and Prisma ORM identifies a state by that hash rather than by a version number.from: (baseline)tells you this is a baseline migration, meaning a first migration that goes from an empty database to a contract state. The word means other things elsewhere, which Baselines sorts out.app space:is the directory the new migration was written to. Your app's migrations are inmigrations/app/, and each Prisma ORM extension package that ships migrations gets a directory of its own beside it. Prisma ORM looks formigrations/in the directory you run commands from, which is normally the project root.
The No db ref set line is telling you that nothing in this project records which contract state your database matches, so migration plan assumed an empty database and planned from there. That assumption is right for a new project. If you already have a database, tell Prisma ORM what is in it before you plan anything, and which command does that depends on what the database holds:
- If Prisma ORM 7 migrated the database, follow the steps in the upgrade guide first.
- If the database is empty, you need nothing:
npx prisma db migratecreates the tables for you, though it does not create the database itself. - If every table in the database already matches your contract, run
npx prisma db signand then read The automatic baseline below.db signchecks the tables against your contract and then writes the marker, which is the record in the database of which contract state it matches. - If the database has only some of your tables, run
npx prisma db init. It adds what is missing and nothing else, and it stops if matching your contract would need a change that could lose data, such as dropping a column. - If this is a development or preview database that you want to match your contract without keeping migration files for it, run
npx prisma db update. Unlikedb init, it will make changes that could lose data, so it asks you to type the database name first.
A migration is a directory, and there is no SQL file in it. migration.ts is the one you read and sometimes edit, and the ops.json and migration.json beside it are what db migrate reads, as What a migration contains describes. Commit all three, along with contract.prisma, contract.json, contract.d.ts, and any new directories under migrations/snapshots/, which are saved copies of your contract. The change itself is in migration.ts, written as a list of method calls:
#!/usr/bin/env -S node
import type { Contract as End } from '../../snapshots/2d2cec8641643159cfc94e499e5eacbd75218e0389d938139b278f68618d6b13/contract';
import endContract from '../../snapshots/2d2cec8641643159cfc94e499e5eacbd75218e0389d938139b278f68618d6b13/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);Everything the migration will do is in that operations list, so reading it tells you exactly what will happen, and editing it is how you change what happens. A change as simple as this one needs no edit. Editing a migration covers the cases where you do edit the file, such as adding a data backfill, reordering operations, or running raw SQL, and the recompile step that makes your edit count.
│ contract: src/prisma/contract.json
│ migrations: migrations/app
│ name: init
✔ Planned 2 operation(s)
ℹ No db ref set — planning from an empty database. Run db init, db update, or db sign if a database already exists.
migrations/app/20260707T1005_init
├─ Create schema "public"
└─ Create table "user"
from: (baseline)
to: 2d2cec8641643159cfc94e499e5eacbd75218e0389d938139b278f68618d6b13
app space: migrations/app/20260707T1005_init
ℹ 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")
);Read the rest of that output closely, because it tells you what was planned and where it was written:
Planned 2 operation(s)counts the steps in the migration, which are listed underneath the directory name, and the DDL preview below them is the SQL those steps run.to:is the hash of your contract. Each version of your contract is a contract state, and Prisma ORM identifies a state by that hash rather than by a version number.from: (baseline)tells you this is a baseline migration, meaning a first migration that goes from an empty database to a contract state. The word means other things elsewhere, which Baselines sorts out.app space:is the directory the new migration was written to. Your app's migrations are inmigrations/app/, and each Prisma ORM extension package that ships migrations gets a directory of its own beside it. Prisma ORM looks formigrations/in the directory you run commands from, which is normally the project root.
The No db ref set line is telling you that nothing in this project records which contract state your database matches, so migration plan assumed an empty database and planned from there. That assumption is right for a new project. If you already have a database, tell Prisma ORM what is in it before you plan anything, and which command does that depends on what the database holds:
- If Prisma ORM 7 migrated the database, follow the steps in the upgrade guide first.
- If the database is empty, you need nothing:
npx prisma db migratecreates the tables for you, though it does not create the database itself. - If every table in the database already matches your contract, run
npx prisma db signand then read The automatic baseline below.db signchecks the tables against your contract and then writes the marker, which is the record in the database of which contract state it matches. - If the database has only some of your tables, run
npx prisma db init. It adds what is missing and nothing else, and it stops if matching your contract would need a change that could lose data, such as dropping a column. - If this is a development or preview database that you want to match your contract without keeping migration files for it, run
npx prisma db update. Unlikedb init, it will make changes that could lose data, so it asks you to type the database name first.
A migration is a directory, and there is no SQL file in it. migration.ts is the one you read and sometimes edit, and the ops.json and migration.json beside it are what db migrate reads, as What a migration contains describes. Commit all three, along with contract.prisma, contract.json, contract.d.ts, and any new directories under migrations/snapshots/, which are saved copies of your contract. The change itself is in migration.ts, written as a list of method calls:
#!/usr/bin/env -S node
import type { Contract as End } from '../../snapshots/2d2cec8641643159cfc94e499e5eacbd75218e0389d938139b278f68618d6b13/contract';
import endContract from '../../snapshots/2d2cec8641643159cfc94e499e5eacbd75218e0389d938139b278f68618d6b13/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);Everything the migration will do is in that operations list, so reading it tells you exactly what will happen, and editing it is how you change what happens. A change as simple as this one needs no edit. Editing a migration covers the cases where you do edit the file, such as adding a data backfill, reordering operations, or running raw SQL, and the recompile step that makes your edit count.
Apply that first migration to your development database by running npx prisma db migrate --advance-ref db. This is the first command here that connects to a database, and it uses db.connection in prisma.config.ts unless you pass a connection string with --db. The --advance-ref db part is what records the result: it points the db ref at the contract state you just applied, so the next time you run migration plan it starts from there and plans only what has changed since.
Now add an optional phone String? field to User, and run both commands again:
bunx prisma contract emit
bunx prisma migration plan --name add_user_phonepnpm prisma contract emit
pnpm prisma migration plan --name add_user_phoneyarn prisma contract emit
yarn prisma migration plan --name add_user_phonenpx prisma contract emit
npx prisma migration plan --name add_user_phone│ contract: src/prisma/contract.json
│ migrations: migrations/app
│ name: add_user_phone
✔ Planned 1 operation(s)
migrations/app/20260707T1006_add_user_phone
└─ Add column "phone" to "user"
from: 2d2cec8641643159cfc94e499e5eacbd75218e0389d938139b278f68618d6b13
to: 967cb9f63587c1d90f64716780f04eec22b263424e89f07139233fc2c3d710c4
app space: migrations/app/20260707T1006_add_user_phone
ℹ DDL preview
ALTER TABLE "public"."user" ADD COLUMN "phone" text;To plan from a contract state other than the one the db ref names, say which one with --from, as in --from 20260707T1005_init. When you give --from a migration directory name, you are naming the contract state that exists once that migration has run. The migration plan reference lists every form the flag accepts.
│ contract: src/prisma/contract.json
│ migrations: migrations/app
│ name: add_user_phone
✔ Planned 1 operation(s)
migrations/app/20260707T1006_add_user_phone
└─ Add column "phone" to "user"
from: 2d2cec8641643159cfc94e499e5eacbd75218e0389d938139b278f68618d6b13
to: 967cb9f63587c1d90f64716780f04eec22b263424e89f07139233fc2c3d710c4
app space: migrations/app/20260707T1006_add_user_phone
ℹ DDL preview
ALTER TABLE "public"."user" ADD COLUMN "phone" text;To plan from a contract state other than the one the db ref names, say which one with --from, as in --from 20260707T1005_init. When you give --from a migration directory name, you are naming the contract state that exists once that migration has run. The migration plan reference lists every form the flag accepts.
Once your first migration is applied, every change you make after that follows the same routine, which does the job migrate dev did in Prisma ORM 7 whenever you wanted migration files out of it, as the command table shows:
- Edit your contract.
- Run
npx prisma contract emit. If you skip this,migration planreads thecontract.jsonfrom before your edit, so it either printsNo changes detectedor plans a migration that is missing your latest change. - Run
npx prisma migration plan --name <name>. - Review what it planned with
npx prisma migration show <dir>, where<dir>is the new migration's directory name. If you editmigration.ts, recompile it before you go any further, because until you do, your edit is not part of what will run. - Apply it. In development that is
npx prisma db migrate --advance-ref db. In CI and production it isnpx prisma db migrate --to <ref>, without--advance-ref, where the ref names the state that environment should reach.
The step that is easy to get out of order is the last one, and the symptom is a migration that repeats work you already planned. If you plan a second migration before you have applied the first one with --advance-ref db, the db ref still names the older contract state, so Prisma ORM plans the first migration's changes a second time, and warns that planning from that state forks the migration graph. To fix that, delete the directory of the repeated migration, run npx prisma db migrate --to <dir> --advance-ref db with the name of the directory whose changes it repeated, and plan again. The --to is needed because your contract already holds the newer change, which has no migration yet. That command applies the first migration if your database does not have it, and points the db ref at it either way. To plan the second migration without applying the first, pass --from <dir> to migration plan instead, which leaves the db ref alone. Deleting the directory is how you discard any migration that has not run on a database, and the matching directory under migrations/snapshots/ can stay.
Commit migrations/app/refs/db.json with the migration files. On a fresh clone it then points at the contract state the team's development databases are at, so the first migration plan chains from there. Without it, a clone that has migrations but no db ref is the last row of the table below, and migration plan stops with an error. When a merge conflicts on that file, take the hash your own database is at, which npx prisma migration status prints next to its @db label, or set the ref afterwards with npx prisma migration ref set db <dir>.
db migrate --advance-ref db is not the only command that updates the db ref. When npx prisma db sign, db init, and db update change the database configured in prisma.config.ts, they point the db ref at the new state as well. db sign is the one to watch, because it updates the ref even when you pass --db to sign a different database, so add --no-advance-ref when you do.
When you leave --from off, what migration plan does depends on whether migrations/app/ already has migrations in it and whether the db ref exists. The combinations are:
Migrations in migrations/app/ |
db ref |
What migration plan does |
|---|---|---|
| None | None | Plans from an empty database |
| None | Exists | Writes a baseline migration, then your change |
| Some | Exists | Plans from the state the db ref points at |
| Some | None | Stops with an error |
That last row is an error because Prisma ORM has migrations but nothing recording which contract state your database matches, and it will not guess. The error prints the commands that resolve it: one sets the db ref, one plans with --from, and one plans with --from @empty.
If you started from an existing database, db sign and db update leave you in the second row of that table: you have no migration files yet, but the db ref names a contract state. What that means in practice is that you should run migration plan before you ever run db migrate. Planning writes a baseline migration ending at the state your database already matches, plus a second migration for your own change if contract.json has changed since. You need that baseline because db migrate fails against the database until one migration's to: hash matches the marker, and it never runs the baseline there, since the marker already records that the database matches that state. Baselines has the full rule.
There is one situation where that automatic baseline is not the one you want. If a deployed database, such as production, matches an older version of your contract than your development database does, a baseline planned the usual way would end at your development database's contract state, which is not the state production matches. Plan the baseline from the older version instead:
- Change your contract back to the deployed version, for example with
git show <commit>:<contract path> > <contract path>. - Run
npx prisma contract emit. - Run
npx prisma migration plan --name baseline --from @empty. - Run
npx prisma db sign --db "$PRODUCTION_DATABASE_URL" --no-advance-refagainst the deployed database. It writes the marker there, and--no-advance-refleaves yourdbref alone. - Restore your current contract and run
npx prisma contract emitagain. - Plan the change with
npx prisma migration plan --name <name> --from <baseline dir>, the baseline's directory name.
None of these steps changes your db ref.
Some changes cannot be planned in full, because the answer depends on your data rather than on your contract. Adding a required field with no default to a table that already has rows is the common one: migration plan writes the ADD COLUMN and the SET NOT NULL, but only you know what the rows that already exist should contain, so it leaves that part for you to fill in as a placeholder. To see one, apply add_user_phone, then add a required displayName String field to User and run both commands again:
bunx prisma contract emit
bunx prisma migration plan --name add_display_namepnpm prisma contract emit
pnpm prisma migration plan --name add_display_nameyarn prisma contract emit
yarn prisma migration plan --name add_display_namenpx prisma contract emit
npx prisma migration plan --name add_display_name│ contract: src/prisma/contract.json
│ migrations: migrations/app
│ name: add_display_name
⚠ Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit
from: 967cb9f63587c1d90f64716780f04eec22b263424e89f07139233fc2c3d710c4
to: 866ab885a8a4bd8d9f5adc47784c068be7895a7182c55cc29f9f9967d3019484
app space: migrations/app/20260707T1008_add_display_nameOpen the new migration.ts and you will find a dataTransform step between the ADD COLUMN and the SET NOT NULL, which is the step that updates the rows already in the table. The two placeholder(...) calls inside it mark where your queries go: the first finds the rows that still need a displayName, and the second fills the value in. Write both with the SQL query builder, then recompile the file, which is the step the output above calls self-emit. Until you fill the placeholders in and recompile, this migration cannot run at all, so a forgotten placeholder costs you a failed deploy rather than a wrong database. Editing a migration walks through writing both queries and recompiling.
│ contract: src/prisma/contract.json
│ migrations: migrations/app
│ name: add_display_name
⚠ Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit
from: 967cb9f63587c1d90f64716780f04eec22b263424e89f07139233fc2c3d710c4
to: 866ab885a8a4bd8d9f5adc47784c068be7895a7182c55cc29f9f9967d3019484
app space: migrations/app/20260707T1008_add_display_nameOpen the new migration.ts and you will find a dataTransform step between the ADD COLUMN and the SET NOT NULL, which is the step that updates the rows already in the table. The two placeholder(...) calls inside it mark where your queries go: the first finds the rows that still need a displayName, and the second fills the value in. Write both with the SQL query builder, then recompile the file, which is the step the output above calls self-emit. Until you fill the placeholders in and recompile, this migration cannot run at all, so a forgotten placeholder costs you a failed deploy rather than a wrong database. Editing a migration walks through writing both queries and recompiling.
You can inspect a planned migration before anything runs, with these commands, none of which connects to a database:
# One migration in detail: operations, metadata, DDL preview
bunx prisma migration show 20260707T1006_add_user_phone
# The whole migration history, with your new migration in place
bunx prisma migration graph
# Integrity check: hashes match, no files missing, every migration starts from empty or where another ends, refs point at known states
bunx prisma migration check# One migration in detail: operations, metadata, DDL preview
pnpm prisma migration show 20260707T1006_add_user_phone
# The whole migration history, with your new migration in place
pnpm prisma migration graph
# Integrity check: hashes match, no files missing, every migration starts from empty or where another ends, refs point at known states
pnpm prisma migration check# One migration in detail: operations, metadata, DDL preview
yarn prisma migration show 20260707T1006_add_user_phone
# The whole migration history, with your new migration in place
yarn prisma migration graph
# Integrity check: hashes match, no files missing, every migration starts from empty or where another ends, refs point at known states
yarn prisma migration check# One migration in detail: operations, metadata, DDL preview
npx prisma migration show 20260707T1006_add_user_phone
# The whole migration history, with your new migration in place
npx prisma migration graph
# Integrity check: hashes match, no files missing, every migration starts from empty or where another ends, refs point at known states
npx prisma migration checkmigration check also tells you the result in its exit code, if you run it from a script. 0 means every check passed, 4 means an integrity failure such as a missing file or an ops.json that someone edited by hand, and 2 means it could not find a migration you named. The one thing it cannot tell you is whether you remembered to recompile after your last edit. To check that yourself, stage the migration with git add, then recompile it. If git status then shows no unstaged change to ops.json or migration.json, they were already up to date. This does not work for a backfill on a model with an updatedAt column. Editing a migration explains why, and how to write that backfill.
[!NOTE] What's early
Planning covers tables, columns, indexes, constraints, and the backfill placeholder shown above. What it cannot do yet is notice that you renamed something: if you rename an optional field, it plans the change as drop column + add column, and
migration planprints a warning that the change can lose data. Take that warning seriously, becausedb migrateruns destructive operations without asking, so readmigration show <dir>before you apply anything. To rename a column and keep the data in it, editmigration.ts, replace that pair of operations with arawSqloperation that runsALTER TABLE ... RENAME COLUMN, and recompile. One more difference from Prisma ORM 7:migration plannever asks you questions the waymigrate devdid.
migration check also tells you the result in its exit code, if you run it from a script. 0 means every check passed, 4 means an integrity failure such as a missing file or an ops.json that someone edited by hand, and 2 means it could not find a migration you named. The one thing it cannot tell you is whether you remembered to recompile after your last edit. To check that yourself, stage the migration with git add, then recompile it. If git status then shows no unstaged change to ops.json or migration.json, they were already up to date. This does not work for a backfill on a model with an updatedAt column. Editing a migration explains why, and how to write that backfill.
Projects created with npm create prisma@latest include the Prisma ORM skills for your coding agent. In an existing project, run npx prisma skills sync. The skills are instruction files your agent reads, so you can ask it 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."
- 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: a blog post on the design of
migration.ts