How migrations work
A migration is how Prisma ORM changes your database when your contract changes. Your contract is the contract.prisma file that replaced schema.prisma, and npx prisma orm init puts it in src/prisma/. After you edit it, run npx prisma contract emit, which replaces prisma generate. Next to contract.prisma it writes the files the rest of Prisma ORM reads: contract.json, which the migration commands read, and contract.d.ts, which your client's types come from.
Prisma ORM 8 supports PostgreSQL and MongoDB. SQLite is experimental, and MySQL is not supported, as Supported databases lists.
You run this loop many times a day:
The migration loopStep 1 of 4
Edit your schema, then emit it. The contract is what every migration command reads.
- Change your contract: edit
contract.prisma, then runnpx prisma contract emit. - Plan a migration:
migration planworks out what has to change in the database by comparing your new contract with an earlier contract state, and writes what it finds as a migration directory undermigrations/app/. A contract state is one version of your contract, named by its hash, so it is how Prisma ORM refers to your contract as it stood at a particular moment. Unless you tell it otherwise, the earlier state it compares against is thedbref, the filemigrations/app/refs/db.jsonnaming the contract state you last applied in development. - Review it: before anything touches the database, run
npx prisma migration show <dir>to see the operations and SQL thatdb migratewill run. If you want the migration to change rows as well, editmigration.tsand recompile it by runningnode migrations/app/<dir>/migration.tsfrom your project root, which rewritesops.json. Nothing extra is needed to run that file, because the Prisma ORM CLI already requires Node.js 22.18 or later, which runsmigration.tsdirectly. - Apply it:
db migratestarts by reading the database's marker, the record in the database of which contract state it matches, so it knows how much of your history the database has already seen, and then runs the migrations from that state to your current contract. In development, add--advance-ref dbso the nextmigration planstarts from what you just applied.
This example uses the contract from Generating a migration, whose User model is stored in the table user because it sets @@map("user"). Its first migration is already applied, with npx prisma db migrate --advance-ref db, and a database connection is set up as in the quickstart. Add an optional phone String? field to User, then run:
bunx prisma contract emit
bunx prisma migration plan --name add_user_phone
bunx prisma migration show 20260707T1006_add_user_phone
bunx prisma db migrate --advance-ref dbpnpm prisma contract emit
pnpm prisma migration plan --name add_user_phone
pnpm prisma migration show 20260707T1006_add_user_phone
pnpm prisma db migrate --advance-ref dbyarn prisma contract emit
yarn prisma migration plan --name add_user_phone
yarn prisma migration show 20260707T1006_add_user_phone
yarn prisma db migrate --advance-ref dbnpx prisma contract emit
npx prisma migration plan --name add_user_phone
npx prisma migration show 20260707T1006_add_user_phone
npx prisma db migrate --advance-ref dbmigration plan prepares the SQL but doesn't run it:
✔ 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;A table name is the model name exactly as written unless the model sets @@map, so without @@map("user") this User model would be the table "User", and a UserProfile model is the table "UserProfile". These are the same names Prisma ORM 7 used.
The app space: line tells you which migration history the new directory was written into. A contract space is a separate migration history, so your own migrations stay in migrations/app/ while each Prisma ORM extension package that ships migrations, such as pgvector, keeps its migrations in its own directory under migrations/.
db migrate applies the migration and confirms what it applied:
✔ Applied 1 migration(s) (1 operation(s)) across 1 contract space(s)The first time you run migration plan, there are no migrations and no db ref on disk yet, so there is no earlier state to compare against and Prisma ORM plans as if the database were empty. If you want it to start somewhere else, pass --from, such as --from 20260707T1006_add_user_phone for the state after that migration. The migration plan reference lists the other forms --from accepts, and The db ref covers what migration plan starts from when you leave --from off.
db migrate needs a database to talk to, and it takes that from db.connection in prisma.config.ts, such as db: { connection: process.env["DATABASE_URL"]! }, unless you pass a URL with the --db flag. In CI and production, run npx prisma db migrate --to <ref>, where the ref names the state that environment should reach, as Applying a migration explains.
A migration directory is named with a timestamp and the --name you passed. migrations/ is in the directory you run commands from, normally your project root:
migrations/
├── app/
│ └── 20260707T1006_add_user_phone/
│ ├── migration.ts # the change, as TypeScript
│ ├── ops.json # the operations, as JSON
│ └── migration.json # where this migration fits in history
└── snapshots/
└── <contract hash>/
├── contract.json # snapshot of one contract state
└── contract.d.ts # types for that snapshot; they type-check migration.tsAfter migration plan, commit the new migration directory, any new directories under migrations/snapshots/, contract.prisma, contract.json, and contract.d.ts.
migration plan writes the change as migration.ts, a TypeScript class with one call per step, such as this.addColumn(...) for a new column, so you can read and change it like any other TypeScript file. Editing a migration explains how to change it.
migration plan also writes the same operations as JSON in ops.json, and ops.json is what db migrate actually runs, never migration.ts, so an edit to migration.ts changes nothing until you recompile it.
migration.json records where this migration fits in your migration history:
- the contract hash it starts
from - the contract hash it ends at,
to - when it was created
- its own hash,
migrationHash, whichnpx prisma migration checkreads
Do not edit migration.json or ops.json by hand, because both are written for you from migration.ts. Edit migration.ts and recompile instead.
Because that from hash is what ties one migration to another, your migration history branches when two people plan migrations from the same contract state on separate branches, and a worked example shows the one migration to plan after the merge.
Inside ops.json, an operation is not only the change itself: it also carries the checks that decide whether the change needs to run and whether it worked. Here are the parts of the operation that adds the phone column:
- Precheck: confirms the database is in the state the change expects, here that
"user"has nophonecolumn. - Execute: the statements that make the change, here
ALTER TABLE "public"."user" ADD COLUMN "phone" text. - Postcheck: confirms the change is in the database, here that the
phonecolumn exists. Some operations have no postcheck.db migrateskips an operation only when its postcheck already passes, so an operation with no postcheck always runs.
When db migrate reaches an operation, it runs the postcheck first. If the postcheck passes, the change is already in the database, so db migrate skips the operation. If it does not pass, db migrate runs the precheck, then the statements, then the postcheck again, and it stops the run if either check fails. Once the operations are done, it checks the database against the contract before it updates the marker, which catches a database that satisfied every individual operation but still doesn't match what you declared.
Say "user" already has a phone column of another type. The postcheck only asks whether a phone column exists, so it passes and the operation is skipped. The check against the contract then finds the wrong type, and the run fails with an error whose code is MIGRATION.RUNNER_FAILED and whose why: line reads The resulting database schema does not satisfy the destination contract. To see which columns differ, run npx prisma db verify --schema-only. Then change the column by hand to the type your contract declares, here text, with ALTER TABLE "public"."user" ALTER COLUMN "phone" TYPE text;, and run db migrate again. Drift covers other changes made outside a migration.
Each operation also has an operationClass, which says what kind of change the operation makes to the database and which you choose yourself when you write a raw SQL operation:
- Additive: adds something new, like a column.
- Widening: loosens a constraint or lets a type accept more values, like dropping
NOT NULLfrom a column. - Destructive: removes or changes something that exists, like dropping a column, and can lose data.
- Data: changes rows, not structure.
Destructive operations are the only class that gets a warning, and it is only a warning: nothing stops and waits for your answer. migration plan prints This migration contains destructive operations that may cause data loss. when it writes the migration, and npx prisma migration show <dir> prints it for any migration on disk, so you see it while reviewing. db migrate runs those operations without asking and prints the warning again afterwards.
Those checks are also what ends a run early when the database isn't in the state your migration assumed, because a precheck stops the run before the change is made, for example when a migration sets NOT NULL on a column that still holds NULL. The error names the operation and the check that failed, so you know exactly which assumption was wrong. When something goes wrong covers what a stopped run leaves behind on each database and how to pick up from it.
The migration commands are part of the Prisma ORM CLI and run as npx prisma <command>. The commands in this table read only files, so they need no database connection:
| Command | What it does |
|---|---|
migration plan |
Generate a migration from your contract changes |
migration new |
Write an empty migration for a data-only or hand-written change |
migration show <target> |
Print one migration's operations, DDL preview, and metadata |
migration list |
List every on-disk migration |
migration graph |
Draw your migration history as a graph |
migration check |
Check that each migration's migrationHash still matches its files and no files are missing, before you commit a hand edit |
<target> is a migration's directory name, its path under migrations/app/, or the first 6 or more characters of its migrationHash.
The commands in this table connect to a database:
| Command | What it does |
|---|---|
db migrate |
Apply pending migrations |
migration status |
Show which contract state the database matches and which migrations are pending |
migration log |
Show the history of migrations the database has actually applied |
If you used Prisma ORM 7, these commands replace its migrate commands:
migrate devbecomesmigration plan, thendb migrate --advance-ref db.migrate devalso had a second job, making a development database match the contract without writing migration files, and that job is nowdb update, which changes the database directly. The db ref lists what each of these does to the ref.migrate deploybecomesdb migrate.migrate resethas no equivalent. Drop and recreate the database with your own tools, then runnpx prisma db migrate --advance-ref dbto run your migrations again, ornpx prisma db initto create what your contract declares without running them.migrate diffbecomesmigration show <dir>, which prints one migration, ordb update --dry-run, which shows the operations that would make a database match the contract.- Baselining, marking an existing database as already migrated, is now
npx prisma db sign, which checks that the tables match your contract before it writes the marker. For a database Prisma ORM 7 migrated, follow Prisma ORM 7 to 8 (PostgreSQL).
Coming from Prisma ORM 7 has the full table, including migrate resolve.
Here is the whole loop, from idea to typed code:
https://www.prisma.io/docs/img/orm/next/migrations/migration-loop.mp4
The commands, the file layout, the migration history, and the checks work the same way on PostgreSQL and MongoDB. On PostgreSQL, operations run SQL statements such as ALTER TABLE. On MongoDB, they create collections, indexes, and JSON Schema validators.
Each database keeps its own marker and ledger, where the ledger is that database's list of every migration applied to it, and migration log is the command that reads it.
Projects created with npm create prisma@latest include the Prisma ORM skills, instruction files for your coding agent. In an existing project, run npx prisma skills sync. Ask your agent to:
- "Add a
phonefield to the User model and plan a migration for it." - "Show me the pending migrations and what SQL they will run."
- "Explain what the ops.json in the latest migration does."
- Studio with Prisma ORM: read your applied migration history: a visual diff, the executed SQL, and a schema diff per migration
- The migration graph: why migrations form a graph
- Generating a migration and Applying a migration: the hands-on loop
- Editing a migration: backfills, raw SQL, and the recompile step
- Rethinking Database Migrations: why this design exists