Rollbacks and recovery
Rolling back is planning one more migration to a state you've already been in. Recovery is fixing the cause and re-running, safely.
Two different situations get called "rollback", and Prisma 8 treats them differently:
- The migration applied, but the change was wrong. You need to move the database back to an earlier state. That's a rollback, and in Prisma 8 it's one more migration.
- The migration failed partway. You need to get unstuck. That's recovery, and it usually means fixing the cause and re-running, because re-running is safe.
Rollback: a migration like any other
There is no migrate down command, and no separate "down migration" files. In the graph model, the state you want to return to is a node you've already visited, so rolling back means planning a new edge that points at it. If you know git, this is git revert, not git reset. History only ever grows, and the ledger (the applied-history record every database keeps) retains the full round trip.
Suppose 20260707T1008_add_display_name shipped and needs to come back out. Plan the reverse edge. <dir>^ means "the state before that migration":
bunx prisma@latest migration plan \
--from 20260707T1008_add_display_name \
--to 20260707T1008_add_display_name^ \
--name rollback_display_name✔ Planned 1 operation(s)
│
└─ Drop column "displayName" from "user" (destructive)
⚠ This migration contains destructive operations that may cause data loss.
from: sha256:e6b5c2849eca8d24ff1e8e88ab2a4234db8e74c497c035cb7ce42e814f31cd63
to: sha256:705b1a62f26f0913caa4bfe3f8b7cb491a1b94bd47fc43471d8711bc480bcbb5
DDL preview
ALTER TABLE "public"."user" DROP COLUMN "displayName";The planner diffs the two contract states and writes the operations that undo the change, flagged destructive because they are. This is a real migration: review it, edit it (for example, to archive the column's data into another table before the DROP), commit it. Then apply it like any other migration:
bunx prisma@latest db migrate --to 20260707T1008_add_display_name^A rollback also leaves a cycle in the graph, so the next migration plan cannot pick its starting point automatically. It fails with MIGRATION.NO_TARGET and lists the reachable states. Plan with an explicit --from <state> until history moves forward again.
One loose end remains after the database moves back: your contract source still contains the change, so db verify reports a hash mismatch until you revert the schema and re-run contract emit. Roll back the contract in the same commit as the rollback migration and the two stay in step.
Afterwards the graph shows the round trip: a forward edge up, a rollback edge back down:
* e6b5c28 @contract
|^ 20260707T1008_add_display_name 705b1a6 -> e6b5c28 3 ops
|v 20260707T1010_rollback_display_name e6b5c28 -> 705b1a6 1 ops
* 705b1a6
|^ 20260707T1005_init - -> 705b1a6 2 ops
* -The database's marker (its record of which graph node it currently matches) is back at 705b1a6, and the ledger records both the apply and the rollback. Nothing was rewritten or deleted.
Two things to be clear-eyed about:
- A rollback does not resurrect data. Dropping the column discards whatever the forward migration and the app wrote into it. If that data matters, edit the rollback migration to save it somewhere first. That's exactly why the rollback is an editable migration rather than an automatic mechanism.
- You don't have to retrace every step. An edge can jump from the current state directly to any earlier node, skipping intermediate states: one planned migration, one apply, even if you're rolling back three changes.
One planning caveat after a rollback
A rollback edge creates a cycle in the graph (A → B → A), and with a cycle the planner can no longer infer "the latest state" on its own. The next time you run migration plan, pass --from explicitly (a migration directory name or hash). The error you'd otherwise get, MIGRATION.NO_TARGET, says exactly this.
Recovery: when a migration fails partway
A failed db migrate run stops at the failing operation and reports it precisely:
✖ Operation alterNullability.setNotNull.user.nickname failed during precheck:
ensure no NULL values in "nickname" (PN-RUN-3000)
Why: Migration runner failed
Fix: Fix the issue and re-run `prisma-cli migrate --to <contract>` — previously applied migrations are preserved.The hint is the CLI's literal output: prisma-cli migrate is its internal name for db migrate, so the command to re-run is npx prisma@latest db migrate --to <contract>.
The playbook:
- Read which check failed. The error names the operation and the specific precheck or postcheck, in plain language. Here, rows with
NULLstill exist, so tightening the constraint would fail. - Nothing to clean up. On PostgreSQL the whole run was one transaction, so the failure rolled it back completely: the database is exactly where it was before the run, and migrations applied in earlier runs are untouched. There is no "half-applied migration" to untangle by hand.
- Fix the cause, then re-run
db migrate. Sometimes the cause is the environment (extension missing, permissions). Sometimes it's the migration itself; here you'd edit the migration to add a backfill before thesetNotNull, recompile withnode migration.ts, and apply again. Re-running is safe, for the reasons covered in the failure model.
The precheck also protects you when a migration works in development but would fail in production. Your dev database had no NULLs, but production does. The precheck halts production before the destructive ALTER touches anything, with an error pointing at the exact rows-with-NULLs condition instead of a generic constraint violation mid-statement.
Drift: when the database isn't where migrations left it
If someone changed the database outside of migrations (a hand-run ALTER, a restore from an old backup), two things can happen. When the change also moved the marker to a state the graph doesn't know, db migrate fails before running any SQL rather than pile changes onto drift. When the marker is intact and only the live schema changed, db migrate still runs: each operation's postcheck decides whether the work is already done, so a hand-applied change it can recognise is skipped, and one it can't fails that operation. Either way, start by finding out where the database really is. Your options, in order of preference:
db verifychecks whether the database still matches your contract, and fails with a precise error when it doesn't.- In development,
db updatereconciles the database directly to your contract without walking the graph. It's quick, but it leaves no record in the migration history, so treat it as a dev-only reset. - For a database with no history at all (a fresh environment, or adopting Prisma 8 on an existing schema),
db initbootstraps it to the current contract and signs the marker.
Reverse planning (--to <dir>^), destructive-operation warnings, resumable re-runs, and the ledger all work today, as shown above. Some gaps remain. There's no rehearsal mode that executes a migration against a shadow copy first. Recovery from drift beyond the db verify/db update/db init trio is manual. The planning caveat after cycles (explicit --from) is a real papercut we expect to smooth out. For deep or unusual situations, #prisma-next on Discord is the fastest route.
Prompt your coding agent
Projects scaffolded with create-prisma@latest install Prisma 8 skills for your coding agent. Ask your agent to:
- "Plan a rollback for the last migration and show me its destructive operations before I decide."
- "This db migrate run failed. Read the error, fix the migration, and re-run it."
- "Check whether staging has drifted from the contract and explain the differences."
See also
- Applying a migration: the failure model in the apply flow
- Editing a migration: fixing a migration that failed for a data reason
- The migration graph: why backwards is just another edge
