How migrations work
Change your contract, plan a migration, review it, apply it. Every step is checked before and after it runs.
A migration is how Prisma ORM changes your database when your contract changes. The contract is the schema description you author (a .prisma file or TypeScript) plus the contract.json artifact that it compiles to. The schema is the actual structure of the database, which a migration changes. For example, if you add a field to a model, something has to run ALTER TABLE against every database that serves your app: your laptop, staging, production. Migrations are that something, recorded as files in your repo so any change is reviewable, repeatable, and versioned with your code.
The workflow is comprised of a loop which you will run many times a day during development:
- Change your contract: edit your
.prismafile, then runcontract emit, which compiles the contract into thecontract.jsonartifact that every other command reads. - Plan a migration:
migration plancompares the new contract against your migration history and writes a migration directory undermigrations/. - Review it: read the generated TypeScript and the DDL preview. Edit the migration if the change requires a data step, then re-run the file to recompile it.
- Apply it:
db migrateruns the pending migrations against your database.
In a project with a database connection configured (the quickstart provides one), the whole loop consists of three commands. Add an optional phone String? field to a model in your .prisma file, then:
bunx prisma@latest contract emit
bunx prisma@latest migration plan --name add_user_phone
bunx prisma@latest db migrate --advance-ref db--advance-ref db moves the ref named db to the state you just applied, which is where the next migration plan starts from. Leave it off and the ref stays where it was, so the next plan starts from that stale state. If no db ref exists at all, the plan has no origin and refuses with MIGRATION.PLAN_ORIGIN_UNKNOWN rather than write a full-create package. In CI and production you leave it off deliberately, because migrations arrive already planned.
migration plan prepares the SQL for the migration, but doesn't run it:
✔ Planned 1 operation(s)
│
└─ Add column "phone" to "user"
DDL preview
ALTER TABLE "public"."user" ADD COLUMN "phone" text;and db migrate applies the migration to your database and confirms what it applied:
✔ Applied 1 operation(s) across 1 contract spaceThe contract-space count only matters once you add database extensions and can be disregarded until then. If you see the ✔ Applied line, you've already run the entire workflow. The rest of this page provides further details on what a migration is, and why it looks the way it does.
What a migration contains
A migration is a directory under migrations/app/, named with a timestamp and a slug:
migrations/
├── app/
│ └── 20260707T1006_add_user_phone/
│ ├── migration.ts # the file you edit
│ ├── ops.json # the file Prisma runs
│ └── 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.tsContract snapshots live once per migrations root, in migrations/snapshots/, keyed by the contract's hash. A migration imports the states it moves between from there (a baseline migration starts from nothing and imports only its end contract), so a contract state shared by several migrations is stored once.
Three files matter day to day. Each has one job:
| File | Purpose |
|---|---|
migration.ts | The file you edit. It describes the schema and data changes as TypeScript function calls. |
ops.json | The file Prisma runs. It contains the compiled migration operations as JSON. |
migration.json | The file Prisma uses to track where this migration fits in history. |
migration.ts: the file you edit
migration.ts is the authoring file. It contains the migration steps as ordinary TypeScript:
this.addColumn(...)
this.createTable(...)
this.dataTransform(...)Because it is TypeScript, type-checked against the contract snapshots it imports from migrations/snapshots/, your editor autocompletes table and column names and catches mistakes before the migration touches a database.
When you want to review or change what the migration does, use this file. For more information, see Editing a migration.
ops.json: the file Prisma runs
ops.json is the compiled form of the migration. When migration.ts runs, Prisma turns the TypeScript steps into JSON operations. The planner runs it for you. After an edit, re-run it yourself with node migration.ts. The migration runner reads only this file.
That means production never executes your TypeScript. It reads the compiled operations, so no application code runs with production credentials.
migration.ts -> ops.json
you edit Prisma runsThe two files are committed side by side and work like package.json and package-lock.json:
package.json -> package-lock.json
what you ask for exact resolved result
migration.ts -> ops.json
what you author exact operations to runmigration.json: the history marker
migration.json records where this migration fits in your database history. Every contract compiles to a deterministic artifact; hashing it gives a short identifier for that exact schema state. A migration records:
- the schema hash it starts
from - the schema hash it moves
to - when it was created
- its own hash, so tampering is detectable
{
"from": "705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5",
"to": "925198f3cc272c5fd19c24ac02f251661775ddac21cdac4e634bbc0dda8b2d72",
"providedInvariants": [],
"createdAt": "2026-07-07T10:06:55.937Z",
"migrationHash": "4b57fa2141c8ad94476d5de66c451468fd6c864210481ad00ac89b259491fbcf"
}Read from and to like a Git commit range: the schema state before, and the schema state after. These hashes are what link migrations into a graph instead of a numbered list. If you want a simple linear migration system, you can ignore the graph entirely.
Together, the three files make a migration both editable and safe to run:
migration.ts you author the change
ops.json Prisma runs the compiled operations
migration.json Prisma tracks the migration in history(providedInvariants is used by extension migrations and stays empty for typical app migrations.)
Every operation checks itself
Inside ops.json, each operation runs in three parts, in order:
- Precheck: confirms the database is in the expected state before the change runs.
- Execute: the statements that make the change.
- Postcheck: confirms the change worked after it runs.
Each operation also carries a class, which drives the (destructive) flags and data-loss warnings you see in CLI output:
- Additive: safe to apply, like adding a column.
- Destructive: can lose data, like dropping a column.
- Data: changes rows, not structure.
{
"id": "column.public.user.phone",
"label": "Add column \"phone\" to \"user\"",
"operationClass": "additive",
"precheck": [
{
"description": "ensure column \"phone\" is missing",
"sql": "SELECT NOT EXISTS (SELECT 1 AS \"one\" FROM \"information_schema\".\"columns\" WHERE (\"table_schema\" = $1 AND \"table_name\" = $2 AND \"column_name\" = $3)) AS \"result\"",
"params": ["public", "user", "phone"]
}
],
"execute": [
{
"description": "add column \"phone\"",
"sql": "ALTER TABLE \"public\".\"user\" ADD COLUMN \"phone\" text"
}
],
"postcheck": [
{
"description": "verify column \"phone\" exists",
"sql": "SELECT EXISTS (SELECT 1 AS \"one\" FROM \"information_schema\".\"columns\" WHERE (\"table_schema\" = $1 AND \"table_name\" = $2 AND \"column_name\" = $3)) AS \"result\"",
"params": ["public", "user", "phone"]
}
]
}This structure is what makes Prisma ORM migrations safe in the situations where classic SQL migrations hurt:
- A migration fails. On PostgreSQL the whole run is one transaction, so a failure rolls everything back and the database returns to exactly where it was. Fix the cause and re-run. Because the runner skips any operation whose postcheck already holds, re-running never double-applies work that's already true in the database. No commenting out statements, no hand-editing the database.
- The database isn't in the state you assumed. The precheck catches it before the change runs, and the error names the exact operation and the exact check that failed, not a generic SQL error halfway through.
- Someone (or an agent) wrote the migration for you. The intent (
migration.ts), the exact SQL (ops.json), and the verification for every step are all in the diff, so review is straightforward.
The commands
These commands live under the Prisma ORM CLI. Run them with npx prisma@latest <command>. The planning and inspection commands work offline. They read files, not your database:
| Command | What it does |
|---|---|
migration plan | Generate a migration from your contract changes |
migration new | Scaffold an empty migration for manual authoring |
migration show <target> | Print one migration's operations, DDL preview, and metadata |
migration list | List every on-disk migration |
migration graph | Draw the migration graph |
migration check | Verify migration files and graph integrity (useful in CI) |
Three commands talk to a database:
| Command | What it does |
|---|---|
db migrate | Apply pending migrations |
migration status | Show where the database is in the graph and which migrations are pending |
migration log | Show the history of migrations the database has actually applied |
Note that applying is db migrate, not migration apply: migration ... commands manage the on-disk migration directories, while db migrate moves a database.
Here is the whole loop in motion, from idea to typed code against the new schema:
The same model for SQL and MongoDB
Everything on this page applies to both database families. On PostgreSQL, operations compile to SQL DDL and the applied state is tracked in a marker, a record Prisma ORM keeps in the database itself naming the contract state the database currently matches. On MongoDB, operations create collections, indexes, and JSON Schema validators, and the marker lives in a _prisma_migrations collection. The commands, the file layout, the graph, and the precheck/execute/postcheck structure are identical.
Migrations are one of the newest parts of Prisma ORM. The core loop on this page (plan, edit, apply, roll back) works today and is exercised in the Prisma ORM test suite. Some things classic migration tools grew over years are not built yet: there is no squash command and no shadow-database dry run. Baselines do exist, both the automatic one and the explicit --from @empty retrofit; see the migration graph. We call out what's missing on each page rather than papering over it.
Prompt your coding agent
Projects scaffolded with create-prisma@latest install Prisma ORM skills for your coding agent. 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."
See also
- Studio with Prisma ORM: read your applied migration history in Prisma Studio: a visual diff, the executed SQL, and a schema diff per migration
- The migration graph: why migrations form a graph and what that buys you
- Generating a migration and Applying a migration: the hands-on loop
- Editing a migration: backfills, raw SQL, and the recompile step
- Rethinking Database Migrations: the blog post on why this design exists
Advanced queries
Use the SQL query builder on PostgreSQL and the pipeline builder on MongoDB for queries the ORM API can't express.
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.
