Database migrations
This guide covers the CMS’s own database: where its schema history lives, how to change it, and how to apply a change to a local database, a deployed one and any per-site databases. Run every command from the repository root.
Where things live
Section titled “Where things live”| Path | What it is |
|---|---|
backend/prisma/migrations/ |
Prisma SQL migrations, one directory per migration. 20260908000000_init is the fresh-install baseline: the whole schema and its seed rows. The source of truth for the schema history, including custom constraints and required seed rows. |
backend/migrations-wrangler/ |
The D1 mirror of those migrations (0001_init.sql for the baseline, then one file per later migration). Generated — never edit it by hand. Wrangler applies these files to D1. |
backend/prisma/prod.prisma, local.prisma, site.prisma, eav.prisma, tables.prisma |
The schema fragments. Edit these. |
backend/prisma/schema.prisma |
Assembled from prod.prisma + site.prisma + eav.prisma + tables.prisma by scripts/build-prisma-schema.mjs. It is checked in, but it is output: an edit made directly to it is overwritten on the next build. |
backend/prisma/schema.local.prisma |
The same assembly on top of local.prisma, used when authoring a migration. Gitignored. |
backend/tenant-migrations/ |
The schema a new per-site database starts from (0001_tenant_schema.sql), plus the deltas under deltas/ that bring existing per-site databases forward. There are no deltas yet. This applies only if you use npm run provision:site. |
After the first release, migrations are append-only. Never edit or delete a released migration. Correct it with a new one.
Before the first release
Section titled “Before the first release”Dee Wan is unreleased, so no installation has to be carried forward yet. Until the first release,
schema changes edit the baseline instead of adding a migration: the fragment, 20260908000000_init,
its 0001_init.sql mirror and the tenant baseline 0001_tenant_schema.sql change together, and
npm run migrate:check, npm run prisma:drift and npm run tenant:schema:check must pass.
The first release starts from fresh databases. It ships no upgrade SQL for databases an earlier
build created, and no tenant delta. A baseline edit changes the tenant schema with no new delta, so
npm run tenant:schema refuses it. Regenerate with npm run tenant:schema -- --allow-no-delta. After
the first release, every schema change is a new migration, made with the steps below.
A local database built from an older baseline does not pick up such a change. Recreate it: stop the
local rig, delete backend/.wrangler/state, then run npm run local:migrate and npm run local:seed
again. This deletes every local site, entry and upload.
Change the schema
Section titled “Change the schema”Before first release, update the baseline as described above. The steps below apply after release.
-
Edit the fragment that holds the model (
site.prisma,eav.prismaortables.prisma). -
Rebuild
schema.prismaand the Prisma client:Terminal window npm run prisma:generate -
Apply everything that already exists to your local database, then write the new migration:
Terminal window npm run local:migratenpm run local:migrate -- initial --create-migrationAuthoring is off unless you pass
--create-migration, so an ordinary apply never writes a migration from a drifting schema. With it, and only when nothing is left to apply locally, the command runsprisma migrate dev --create-only, which writes a new directory underbackend/prisma/migrations/, and then applies that migration to the local database. It uses the tracked local config (backend/wrangler.dev.jsonc), so it works on a clone that has never been installed.Keep the word
initialin that command.--create-migrationgiven alone is currently rejected with a usage message. -
Read the generated SQL before you commit it. A migration authored from a schema that has drifted from the database can contain
DROP TABLEstatements nobody intended. -
Regenerate the D1 mirror and check the result:
Terminal window npm run migrate:sync # writes backend/migrations-wrangler/ from the Prisma migrationsnpm run migrate:check # fails if the mirror and the migrations disagreenpm run prisma:drift # fails if schema.prisma no longer describes what the migrations build -
If you use per-site databases, regenerate their schema too:
Terminal window npm run tenant:schema # rewrites the per-site bootstrap schemanpm run tenant:schema:check # fails until every new table has a placementA migration that adds a table fails
tenant:schema:checkuntil the table is placed inbackend/src/lib/tenancy/placement.ts.A migration that changes what existing per-site databases hold also needs a delta. Write the SQL that brings such a database forward as
backend/tenant-migrations/deltas/NNNN_<name>.sql, with the next number, before you runnpm run tenant:schema. For an added column or index this is the migration’s own SQL.npm run tenant:schemarefuses a changed tenant schema with no new delta and names the file to write. Pass--allow-no-deltaonly when the change needs none. -
Commit the Prisma migration, its mirror file, any delta and any tenant schema change together.
Apply migrations
Section titled “Apply migrations”| Where | Command | Notes |
|---|---|---|
| The local rig (no Cloudflare account) | npm run local:migrate |
Uses backend/wrangler.dev.jsonc, which every clone has. Needs no credential. |
| The local mirror of an installed configuration | npm run migrate:d1:local |
Reads backend/wrangler.jsonc, which setup writes and git ignores, so it fails on a clone that has not been installed. Use it with npm run dev:install --workspace backend. |
| The deployed control database | npm run migrate:d1 |
Needs CLOUDFLARE_API_TOKEN. Prints the config and database it is about to migrate before it runs. Add -- --config <file> for an installation that is not backend/wrangler.jsonc. |
| Existing per-site databases | npm run tenant:upgrade |
Only if you ran npm run provision:site. -- --plan shows what each site is missing without writing. |
A fresh installation needs none of these: npm run setup applies the schema itself.
npm run deploy refuses to deploy the admin Worker while the control database has pending
migrations, or when it cannot tell. Apply them first. --skip-migration-check overrides the check.
Use it only when you know the new code does not depend on the pending migrations.
When a migration fails
Section titled “When a migration fails”- Migrations are applied in order, and each one is recorded as applied only after it succeeds. A failure stops the run and says the migration was not recorded. The migrations after it are not applied. Fix the cause and run the same command again: it retries the failed migration.
- If a migration stopped part-way, the retry runs it statement by statement and skips only the
statements whose table, index, trigger or column already exists, so it can finish. A migration
that contains a
BEGIN … ENDblock other than a trigger cannot be split that way. The run refuses and names it, and you resolve the schema by hand before running again. - Do not fix a failed migration by editing one that has already been applied somewhere. Add a new
migration that corrects it, run
migrate:sync, and apply again. - Before applying a risky migration to a deployed database, take an export:
npx wrangler d1 export <database-name> --remote --output backup.sqlfrombackend/. Cloudflare D1 also keeps its own point-in-time history (Time Travel), which can restore a database to a moment before the migration. - If
npm run deployrefuses because of pending migrations, apply them withnpm run migrate:d1rather than bypassing the check.
Unreleased / pre-MVP; no stable version
Source revision: fe23db481c422b40e317b92fc98116b4c0c26a87 (modified checkout)
Build date (UTC): 2026-10-18T09:22:34.630Z
Upgrade target: unavailable: modified checkout. Release notes