Prisma 8 is here.Read the docs

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.

A rollback is a new migration, not rewritten historyStep 1 of 3
add_display_namerollback (a new migration)705b1a6before the changee6b5c28after add_display_name
add_display_name applies cleanly and the database sits at the new state. Then the team decides the change was wrong.

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:

  1. Read which check failed. The error names the operation and the specific precheck or postcheck, in plain language. Here, rows with NULL still exist, so tightening the constraint would fail.
  2. 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.
  3. 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 the setNotNull, recompile with node 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 verify checks whether the database still matches your contract, and fails with a precise error when it doesn't.
  • In development, db update reconciles 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 init bootstraps it to the current contract and signs the marker.

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

On this page