Prisma Next — Migration Authoring
Edit your data contract. Prisma Next plans the migration. You fill in any data transforms.
The three-step user model:
- You edit your data contract. ()
- Prisma Next plans the migration for you. ← this skill
- If a data transform is needed, you edit and self-emit. ← this skill
Once the contract changes, you choose how the change reaches the database. This skill covers the two paths (
and
+
), the migration-package contract, the
authoring API, and the failure modes you recover from without leaving the loop.
When to Use
- User edited the contract and wants to apply the change to the DB.
- User wants to author a migration with a data transform.
- User wants to run pending migrations against a local DB.
- User hit , (unfilled placeholder), or a partially-applied migration.
- User mentions: migrate, migration, db push, db update, , , drift, hash mismatch, data backfill.
When Not to Use
- User wants to know what migrations will run on deploy / on merge, or to manage refs and invariants →
prisma-next-migration-review
.
- User wants to edit the contract → .
- User wants a deeper read of a single structured error envelope → .
Key Concepts
- (quick path). Reads the emitted contract, diffs against the live DB, applies the change. Optional prints the plan without executing. Interactive destructive-op confirmation (or to auto-accept). Writes no migration directory. Operations needing data transforms are not handled by this path — excludes the operation class entirely and short-circuits where a data transform would be required. Use only against a database that has no shared history with anyone else (your local dev DB).
- (formal path). Reads the emitted contract, diffs against the head of the on-disk migration graph, writes a new migration package under
migrations/app/<YYYYMMDDTHHMM>_<snake_slug>/
. If any operation needs a data transform, the package's contains calls you fill in.
- The segment in migration paths is the consuming application's contract-space id. Every migration you author lives under . Extensions your contract depends on get their own sibling directories (
migrations/<extension-space-id>/
) — those are managed by the extension package and you don't write into them. The segment lands automatically the first time you run / against an app-level config.
- Migration package files (inside each ):
- — manifest (metadata + ).
- — canonical operation list. Content-addressed; is computed over this.
- and — the contract this migration ends at, imported by for type-safe data transforms.
- — TypeScript authoring source, framework-rendered by (or ). You edit specific holes in it (see Fill a placeholder below) and re-emit / by running it.
- Self-emit. Running
node migrations/app/<dir>/migration.ts
regenerates and from the (possibly edited) TS source. This is the only supported way to update an existing migration package after edits.
- shape. Framework-rendered. A class extending (re-exported by
@prisma-next/target-postgres/migration
in the rendered import line — see the framing block below), with an getter that returns an array of factory-call values. The file ends with MigrationCLI.run(import.meta.url, M)
so executing it self-emits.
- . A sentinel the planner emits into the rendered (imported from
@prisma-next/target-postgres/migration
on the framework-managed import line) wherever a data transform is needed. Calling at emit time throws Unfilled migration placeholder. The user replaces the arrow with a real query-plan closure, then self-emits.
this.dataTransform(endContract, name, { check, run })
. The data-transform factory. is a rowset query whose presence-of-any-row signals "work remains"; is one or more mutation queries that perform the backfill. Both are lazy closures returning query-plans built against . The runner wraps as for precheck and for postcheck, so the same closure asserts both "there is work" and "the work is done".
- . A boolean field on the JSON result of . means the package was written but contains unfilled placeholders — will throw until you edit and self-emit.
- . Content-addressed identity of a migration package. fires when the stored hash in disagrees with the hash recomputed from the on-disk files (almost always: someone edited without self-emitting).
- Marker. A single row in the table that records "this database is at contract hash X for space Y". Each successful migration advances the marker as part of the same transaction as the DDL. writes the marker from the current contract hash, but only after a schema-verification pass succeeds (it will not sign a database whose live schema disagrees with the contract).
- Apply runs in a transaction. Each migration runs inside . On any failure mid-migration, Postgres rolls the migration back; the marker stays at the previous migration's hash. The database is not left in a half-applied state for ordinary DDL + data-transform sequences.
- Operation classes. Every operation declares an : , , , or . The CLI surfaces these in the plan preview and in JSON output. There is no class and the framework does not emit
CREATE INDEX CONCURRENTLY
— operations stay transactional.
is framework-rendered, not hand-authored
Files under
migrations/<space-id>/<timestamp>/migration.ts
(for your own app,
is always
) are
rendered for you by the framework —
prisma-next migration plan
writes a populated package whenever the contract changes, and
prisma-next migration new
writes an empty scaffold when you want to author operations directly. You do not write these files from scratch. You edit specific holes the framework leaves behind — chiefly replacing
sentinels with real
this.dataTransform({ check, run })
closures — then self-emit.
The imports at the top of the rendered file currently point at
@prisma-next/target-postgres/migration
. The user-facing
façade does
not currently re-export this surface, so the rendered import deliberately reaches into the target package directly. Linear ticket
TML-2526 tracks closing that gap; once it lands, the renderer (and this skill) will switch to
@prisma-next/postgres/migration
in one step.
Until then, treat the rendered import line as framework-managed:
- Leave it where it is. Don't rewrite it to a different path; the framework's renderer is the authoritative shape and any change you make by hand will be reverted (and may trip ) the next time the package is re-rendered or self-emitted.
- If you need an additional symbol (e.g. , , ) to fill in a placeholder, add it to the existing rendered import line rather than introducing a second import from a different subpath.
- The "user code imports only from " convention applies to your own modules (queries, runtime setup, contract authoring). The framework-rendered scaffold is the framework's surface, not yours; the rule is suspended for that one file.
Diagnostic codes you route on
| Code | Source | Move |
|---|
| Unfilled migration placeholder | Throwing at emit time | Open , replace the named call with the real query closure, self-emit. |
| migration.ts not found | Reading a migration package | The package is malformed. Recover from version control, or run prisma-next migration new
for a fresh one. |
| invalid default export | Loading | The file's default export is not a subclass or factory function. Restore the planner-emitted scaffold from version control or re-run for a clean package. |
| dataTransform contract mismatch | Building a data-transform query plan | The query builder was instantiated with a contract reference different from the passed to . Use the imported at module scope for both. |
| Migration package is corrupt | (or any read of the package) | / were edited without self-emitting. Run node migrations/app/<dir>/migration.ts
to re-emit, then re-apply. |
| Hash mismatch | | The marker in disagrees with the contract hash. The DB is at a different contract version than the code thinks. Either run a migration forward, or — if the DB is correct and the marker is stale after a manual fix-up — run . |
| Database not signed | Any command needing a marker | The DB has no marker yet. Run prisma-next db init --db <url>
to baseline an empty database, or to apply the current contract directly. |
Decision — which path do you take?
| Situation | Path | Why |
|---|
| Local dev, schema in flux | | Fast, interactive, no migration files. |
| Shared branch with other developers | + | Replayable, reviewable, content-hashed. |
| Anything reaching production | + | Production must run a reviewed, hashed migration. |
| Adding a column that needs a backfill | (writes ), edit , self-emit, then | does not author data transforms; the formal path does. |
| Recovering from drift (DB diverged from contract) | after manual fix, or if PN can plan the fix | Depends on which side is right. See Recover from drift below. |
Workflow — (quick path)
The concept:
resolves the destination (
) against the live DB and applies the difference. Preview with
. Destructive ops prompt interactively unless you pass
or
. The path excludes operations of the
class entirely — if the diff requires a data transform,
fails with a planning error and you switch to
to author the transform.
Run after a contract edit:
bash
pnpm prisma-next contract emit
pnpm prisma-next db update --db $DATABASE_URL --dry-run
pnpm prisma-next db update --db $DATABASE_URL
pnpm prisma-next db verify --db $DATABASE_URL
Inspect the JSON output to drive the next move:
bash
pnpm prisma-next db update --db $DATABASE_URL --json
The JSON contains
with each
, plus (in apply mode)
execution.operationsExecuted
and the post-apply
. If the command failed because of destructive operations, the error envelope's
meta.destructiveOperations[]
lists exactly what would have been dropped.
Workflow — + (formal path)
The concept:
writes a new migration package on disk. If the planner needed any data transforms, the package is
pending —
holds
calls until you fill them in.
runs every pending package in graph order, transactionally.
Plan a change:
bash
pnpm prisma-next contract emit
pnpm prisma-next migration plan --name <snake_slug>
Read the result. The JSON shape exposes the queryable signals:
- — the path of the new package (e.g.
migrations/app/20260515T1200_add_user_email/
).
- — if still contains calls.
operations[].operationClass
— for spotting and ops.
- — family-agnostic textual preview.
Inspect the package:
bash
pnpm prisma-next migration show
pnpm prisma-next migration show <dirName-or-migrationHash-prefix>
Fill in any data transforms (see
Fill a placeholder), self-emit if you edited
, then:
bash
pnpm prisma-next migrate --db $DATABASE_URL
pnpm prisma-next db verify --db $DATABASE_URL
runs without prompting — destructive-op confirmation lives on
, not here. Review destructive ops in the plan output or in
before applying.
Workflow — Fill a placeholder
The concept: the planner can detect
that a data transform is needed (e.g. backfilling a new
column with no default) but not
what it should do. It writes a typed scaffold and stops; you fill the check and run closures with real query plans built against
, then self-emit.
The scaffold the planner emits looks like:
typescript
// migrations/app/20260515T1200_add_user_name/migration.ts
import endContract from './end-contract.json' with { type: 'json' };
import { Migration, MigrationCLI, addColumn, placeholder } from '@prisma-next/target-postgres/migration';
export default class M extends Migration {
override get operations() {
return [
addColumn('public', 'user', {
name: 'name',
typeSql: 'text',
defaultSql: '',
nullable: true,
}),
this.dataTransform(endContract, 'backfill user.name', {
check: () => placeholder('backfill user.name:check'),
run: () => placeholder('backfill user.name:run'),
}),
];
}
}
MigrationCLI.run(import.meta.url, M);
Replace both
calls with query-plan closures built from
. The
closure must return a
rowset query whose presence of any row signals "work remains" — conventionally
<table>.select('id').where(<violation predicate>).limit(1)
. Scalar/aggregate shapes (
,
) silently break the contract: the runner wraps
twice (
for precheck,
for postcheck), and a query that always returns one row makes
always true and
always false.
Build the query builder against
so the storage hashes line up — using a different contract reference raises
. The filled-in shape (the rendered scaffold above with
calls replaced; if you need an extra factory like
, add it to the
existing @prisma-next/target-postgres/migration
import line rather than authoring a second import). See
for the surrounding
setup:
typescript
import endContract from './end-contract.json' with { type: 'json' };
import { Migration, MigrationCLI, addColumn, setNotNull } from '@prisma-next/target-postgres/migration';
import { db } from './db'; // sql({ context: createExecutionContext({ contract: endContract, ... }) })
export default class M extends Migration {
override get operations() {
return [
addColumn('public', 'user', {
name: 'name',
typeSql: 'text',
defaultSql: '',
nullable: true,
}),
this.dataTransform(endContract, 'backfill user.name', {
check: () => db.users.select('id').where((f, fns) => fns.eq(f.name, null)).limit(1),
run: () => db.users.update({ name: '' }).where((f, fns) => fns.eq(f.name, null)),
}),
setNotNull('public', 'user', 'name'),
];
}
}
MigrationCLI.run(import.meta.url, M);
Self-emit:
bash
node migrations/app/20260515T1200_add_user_name/migration.ts
Self-emit regenerates
and recomputes
in
. The next
will see a consistent package.
Workflow — Author a migration by hand
The concept: the same
class shape lets you author operations directly when the planner has nothing to plan (a custom data fix, an extension install, a baseline). Even here you don't write the file from scratch —
renders an empty package for you, and you edit the
getter inside it, then self-emit.
bash
pnpm prisma-next migration new --name <snake_slug>
The factories you can call are re-exported through the same framework-rendered
@prisma-next/target-postgres/migration
import line — add the names you need to that line rather than introducing a second import. Categories (browse with
on the command and by reading the import list):
- Tables: , .
- Columns: , , , , , , .
- Constraints: , , , .
- Indexes: , .
- Enums: , , , .
- Dependencies: , , .
- Raw escape hatch:
rawSql({ id, label, operationClass, target, precheck, execute, postcheck, ... })
.
- Data transforms:
this.dataTransform(endContract, name, { check, run })
(instance method, not a free factory).
Self-emit (
node migrations/app/<dir>/migration.ts
) after each edit.
Workflow — Inspect the live schema
The concept:
is read-only and never writes files. It prints the live schema as a tree by default or as JSON with
. Use it during planning and as part of verification.
bash
pnpm prisma-next db schema --db $DATABASE_URL
pnpm prisma-next db schema --db $DATABASE_URL --json > schema.json
There is no built-in filter flag — pipe the JSON through
(or your favourite JSON tool) if you only want one table.
Workflow — Verify contract vs DB
The concept:
compares the live schema
and the marker against the contract. Three modes:
- Default — full verification (schema + marker).
- — skip schema verification, only check the marker.
- — skip marker verification, only check schema satisfies contract.
- adds: schema elements not present in the contract are an error (default is "DB may have extras").
bash
pnpm prisma-next db verify --db $DATABASE_URL
On mismatch, the error envelope names the failure mode (
hash mismatch,
marker missing, target mismatch, schema issues with structured paths). Run after any manual fix or any
.
Workflow — Re-sign the marker
The concept:
rewrites the marker to the current contract hash. Use after a manual repair where the DB is the source of truth and the marker is stale.
performs a schema-verify first and refuses to sign a DB whose schema disagrees with the contract — so a successful sign always means the schema matches and the marker is now correct.
bash
pnpm prisma-next db sign --db $DATABASE_URL
Workflow — Recover from drift
The concept: drift means
reports the live DB schema doesn't match what the marker says it should be. Two valid moves, picked by which side is correct:
- The contract is right; the DB is wrong → run a migration. Either (quick path, dev DB only) or + (everywhere else).
- The DB is right; the contract or marker is wrong → edit the contract to match the DB (see ), emit, then to refresh the marker.
The diagnostic that reveals which side is right:
bash
pnpm prisma-next db schema --db $DATABASE_URL --json
pnpm prisma-next db verify --db $DATABASE_URL --json
Re-verify after either branch. Stop when
returns
with no diagnostics.
Workflow — Recover from a partially-applied migration
The concept: each migration applies transactionally, so a mid-migration failure rolls back to the previous successful migration. The marker stays at the previous migration's
hash. The diagnostic move is to verify state, fix the failed migration's
, self-emit, and re-apply.
Failures that
can leak partial state are limited to: explicit
operations that step outside a transaction (e.g. inside an
step that runs against an autocommit driver session), or external side-effects (calls out to other systems from a
closure). For ordinary DDL +
flows the runner's transaction wrap keeps the database consistent.
Diagnose:
bash
pnpm prisma-next db verify --db $DATABASE_URL --json
pnpm prisma-next db schema --db $DATABASE_URL --json
Fix and re-apply:
bash
node migrations/app/<dir>/migration.ts
pnpm prisma-next migrate --db $DATABASE_URL
If the failure was an out-of-band side-effect that left external systems half-changed, repair those by hand before re-applying.
Workflow — Recover from
The concept:
is content-addressed. A mismatch means
's stored hash disagrees with the hash recomputed from
(and metadata). The cause is almost always: someone edited
and forgot to self-emit. The remediation is to self-emit the offending package.
bash
node migrations/app/<dir>/migration.ts
pnpm prisma-next migrate --db $DATABASE_URL
If self-emit itself fails (e.g. the contract has moved on and the operations no longer make sense against
), the package is stale. Either restore it from version control or delete it and re-plan with
.
Workflow — Resolve a destructive-operation prompt ( only)
The concept: when
would drop columns or tables, it stops and asks before applying. The prompt is
-specific —
does
not prompt and runs whatever the migration package contains, so review the plan or call
before applying.
When
reports destructive operations interactively, the warning lists them. The prompt is:
Apply destructive changes? This cannot be undone.
Routing:
- Answer yes if the data is no longer needed.
- Answer no, then either:
- Re-shape the migration via and hand-edit to preserve the data (e.g. copy-to-new-column, then drop), or
- Skip the destructive operation by reverting the contract change.
In non-interactive contexts (CI,
,
), the destructive-op response is returned as a structured error —
meta.destructiveOperations[]
lists what would have been dropped. Re-run with
to auto-accept, or address each operation individually.
Common Pitfalls
- Using against shared or production databases. Never. The change leaves no migration history. Use + .
- Skipping a data transform. Leaving in makes the next throw . Fill every placeholder slot and self-emit.
- Editing directly. It's the canonical artifact, not the authoring source. Edit , then self-emit.
- Forgetting to self-emit after editing . The next either uses the stale (if you only added comments) or fails with (if you changed operations). Always self-emit.
- Aggregate closure in . Returning or breaks the precheck/postcheck contract — both sides resolve to constants. Use a rowset shape:
select('id').where(<violation>).limit(1)
.
- Two contract references in one migration. Building a query plan against a different contract than the one passed to
this.dataTransform(endContract, ...)
raises . Always import once at module scope and use the same reference.
- Renaming and expecting the planner to detect it. Prisma Next has no in-contract rename hint today; the planner emits a destructive drop+add. Hand-edit to rewrite the destructive op as a that issues
ALTER TABLE ... RENAME COLUMN ...
(or use the two-migration keep / backfill / drop pattern), then self-emit. See § Edit a field — rename.
- Hand-authoring from a blank file, or rewriting the rendered import line. Migration files are framework-rendered — let
prisma-next migration plan
(or ) render the package, then edit only the holes the framework leaves for you. The rendered @prisma-next/target-postgres/migration
import is the framework's surface, not a stable user-facing one (TML-2526 tracks moving it to @prisma-next/postgres/migration
); leave the path alone, and add any extra symbols you need to the existing import line.
What Prisma Next doesn't do yet
- Runtime-apply migrations. Prisma Next doesn't apply pending migrations from your app's startup code (the "Drizzle pattern" for serverless / edge). Workaround: run from your deploy pipeline before the app starts. If you need runtime-apply built-in, file a feature request via the skill.
- Seeds-as-first-class. Prisma Next doesn't ship a equivalent. Workaround: write a TypeScript script that imports your instance and runs your setup queries; invoke it from 's scripts. If you need first-class seeding, file a feature request via the skill.
- Migration squashing. Prisma Next doesn't squash older migrations into a baseline. They accumulate; for very large histories, manual baseline-and-truncate is the path. If you need built-in squashing, file a feature request via the skill.
- In-contract rename hints. The planner cannot detect that a field rename is a rename rather than a drop+add. Workaround: hand-edit to issue a via , or use a keep / backfill / drop pattern across two migrations. If you need a contract-level rename hint, file a feature request via the skill.
- façade re-export of the migration surface. The package does not yet re-export the migration authoring API (, , , , , , …). The framework-rendered therefore imports from
@prisma-next/target-postgres/migration
directly. Workaround: leave the rendered import where it is — it works, it is what the framework emits, and rewriting it breaks round-tripping. Linear ticket TML-2526 tracks closing the gap; once it lands, the renderer will switch and existing files can be migrated by re-running the renderer. If this gap is biting you, file a follow-up via the skill referencing TML-2526.
Checklist