Skip to content
Dee Wan 1.0 is not yet released (pre-MVP). See the 1.0 product boundary.

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.

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.

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.

Before first release, update the baseline as described above. The steps below apply after release.

  1. Edit the fragment that holds the model (site.prisma, eav.prisma or tables.prisma).

  2. Rebuild schema.prisma and the Prisma client:

    Terminal window
    npm run prisma:generate
  3. Apply everything that already exists to your local database, then write the new migration:

    Terminal window
    npm run local:migrate
    npm run local:migrate -- initial --create-migration

    Authoring 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 runs prisma migrate dev --create-only, which writes a new directory under backend/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 initial in that command. --create-migration given alone is currently rejected with a usage message.

  4. Read the generated SQL before you commit it. A migration authored from a schema that has drifted from the database can contain DROP TABLE statements nobody intended.

  5. Regenerate the D1 mirror and check the result:

    Terminal window
    npm run migrate:sync # writes backend/migrations-wrangler/ from the Prisma migrations
    npm run migrate:check # fails if the mirror and the migrations disagree
    npm run prisma:drift # fails if schema.prisma no longer describes what the migrations build
  6. If you use per-site databases, regenerate their schema too:

    Terminal window
    npm run tenant:schema # rewrites the per-site bootstrap schema
    npm run tenant:schema:check # fails until every new table has a placement

    A migration that adds a table fails tenant:schema:check until the table is placed in backend/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 run npm run tenant:schema. For an added column or index this is the migration’s own SQL. npm run tenant:schema refuses a changed tenant schema with no new delta and names the file to write. Pass --allow-no-delta only when the change needs none.

  7. Commit the Prisma migration, its mirror file, any delta and any tenant schema change together.

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.

  • 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 … END block 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.sql from backend/. 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 deploy refuses because of pending migrations, apply them with npm run migrate:d1 rather 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