Schema Evolution in Production

Prisma Intermediate/Advanced

Chapter 8 ยท Schema Evolution in Production

In development, a bad migration costs a migrate reset. In production, the database holds real data and the app is running while you change it. This chapter covers changing a live schema safely: which changes are risky, how to rename without losing data, how to change a column's shape without downtime, how to move data, how migrations run in CI, and how to bring an existing database under Prisma Migrate.

Development vs. Production Commands

CommandWhereWhat it does
migrate devDevelopment onlyCreates new migrations from schema changes and applies them; may offer to reset the database
migrate dev --create-onlyDevelopmentWrites the migration SQL without applying it, so you can edit it first
migrate deployCI, staging, productionApplies pending migrations from the migrations folder; never creates new ones or resets
migrate statusAnywhereShows which migrations have and haven't been applied
migrate resolveRecoveryMarks a migration as applied or rolled back after you fix a problem by hand
Never run migrate dev against production
migrate dev is built for throwaway databases. If it detects drift, it offers to reset — which deletes everything. Production only ever gets migrate deploy, running migration files that have already been reviewed and tested.

Which Changes Are Risky?

ChangeRiskSafe approach
Add a table, or an optional columnLowJust migrate
Add a required columnFails on existing rows unless it has a defaultAdd with @default, or optional → backfill → required
Add an indexCan lock a big table while buildingOn PostgreSQL, edit the SQL to CREATE INDEX CONCURRENTLY for large tables
Rename a column or tablePrisma generates drop + add: data lossEdit the SQL to a rename, or use @map
Change a column's type or meaningOld code breaks during deployExpand and contract
Drop a column or tableIrreversible; running code may still use itStop using it first, deploy, then drop

Renaming Without Losing Data

Rename viewCount to views in the schema and Prisma sees one field disappear and another appear. The migration it writes drops the old column — and every view count with it — and adds an empty new one. The fix, from Prisma's own docs, is to create the migration without applying it and edit the SQL:

npx prisma migrate dev --name rename-viewcount --create-only
-- Generated (destroys data): -- ALTER TABLE "Post" DROP COLUMN "viewCount", -- ADD COLUMN "views" INTEGER NOT NULL DEFAULT 0; -- Edited (keeps data): ALTER TABLE "Post" RENAME COLUMN "viewCount" TO "views";
npx prisma migrate dev # applies the edited migration
Or don't rename the column at all
If you only want a nicer name in your code, keep the database column and map to it: views Int @default(0) @map("viewCount"). The TypeScript field is views; the column is untouched and no migration SQL is needed. @@map does the same for table names.
A rename still breaks running code
Even a correct rename is a problem mid-deploy: old copies of the app, still running, query viewCount, which no longer exists. For a live app with no downtime, use expand and contract instead.

Expand and Contract

The core technique for zero-downtime changes: never make a change that the currently running code can't handle. Instead, go in small steps, each one deployable on its own. Example: replacing published Boolean with a richer status (DRAFT, PUBLISHED, ARCHIVED).

StepSchemaCode
1. ExpandAdd status PostStatus? (optional)Unchanged
2. Dual write—Every write sets both published and status
3. BackfillA data migration fills status for old rows—
4. Switch readsMake status requiredRead status; still write both
5. Stop old writes—Only write status
6. ContractDrop published—

It's slower than one big change, and each step is a separate deploy. In exchange, at every moment both the old and the new version of the app work against the database, and any step can be paused or rolled back. The capstone in Chapter 10 walks through this exact change.

Data Migrations

Step 3 above changes data, not structure. Prisma Migrate only generates schema changes, but a migration is just SQL, so you can write the data change yourself in an empty migration:

npx prisma migrate dev --name backfill-post-status --create-only
-- prisma/migrations/2026..._backfill-post-status/migration.sql UPDATE "Post" SET "status" = CASE WHEN "published" THEN 'PUBLISHED'::"PostStatus" ELSE 'DRAFT'::"PostStatus" END WHERE "status" IS NULL;
  • Write data migrations so they're safe to re-run (WHERE "status" IS NULL).
  • On a very large table, one UPDATE can lock rows for a long time. Backfill in batches from a script (using Chapter 3's raw SQL or updateMany with a take-style ID range) instead of one migration.
  • Test the migration on a copy of production data before running it for real.

Migrations in CI and Deployment

# In CI, on every pull request npx prisma validate # schema is valid npx prisma migrate diff \ --from-migrations prisma/migrations \ --to-schema prisma/schema.prisma \ --exit-code # fails if the schema has changes with no migration # At deploy time, before starting the new app version npx prisma migrate deploy

The migrate diff check catches the classic mistake of editing schema.prisma and forgetting to commit a migration. migrate deploy should run once per deployment, from one place — a release step or job — not from every copy of the app as it starts up, where several could try to migrate at once.

Commit migrations; never edit applied ones
The prisma/migrations folder is part of your code. Once a migration has run anywhere shared, treat it as permanent. Prisma records a checksum of each applied migration, and editing one afterwards causes errors about modified migrations. To fix a mistake, write a new migration.

Adopting Prisma on an Existing Database

Joining a project with a database that was never managed by Prisma takes two steps: generate a schema from it, then tell Migrate that this is the starting point.

1. Introspect with db pull

npx prisma db pull

Prisma reads the tables, columns, keys and indexes and writes matching models into schema.prisma. Tidy the result — rename models and fields to your conventions using @map and @@map, so the database itself doesn't change.

2. Baseline

Migrate would otherwise try to create every table again. A baseline migration describes the existing database and is marked as already applied:

mkdir -p prisma/migrations/0_init npx prisma migrate diff \ --from-empty \ --to-schema prisma/schema.prisma \ --script > prisma/migrations/0_init/migration.sql npx prisma migrate resolve --applied 0_init # run against each existing database

From then on, new schema changes become ordinary migrations on top of 0_init. A brand-new database (a new developer, or a test database) gets 0_init applied for real, so every environment ends up with the same structure.

Hands-On Exercises

Exercise 1

Rename Post.content to body twice: once with an edited migration, once with @map. Show the SQL for each, and say which you'd choose for an app with several copies running.

๐Ÿ“„ View solution
Exercise 2

Add a required excerpt String to Post on a database with existing posts, without downtime and without a meaningless default. Plan the migrations and deploys, with the SQL.

๐Ÿ“„ View solution
Exercise 3

Write a CI workflow (GitHub Actions or similar) that fails a pull request with a missing migration, and a deploy step that applies migrations exactly once before the new version starts.

๐Ÿ“„ View solution

Chapter 8 Quick Reference

  • migrate dev in development only; migrate deploy everywhere else
  • Renames: --create-only, edit to RENAME COLUMN; or keep the column and use @map/@@map
  • Expand → dual write → backfill → switch reads → stop old writes → contract
  • Data migrations: SQL in an empty --create-only migration; re-runnable; batch big tables
  • CI: migrate diff --from-migrations ... --to-schema ... --exit-code; deploy: migrate deploy once
  • Never edit an applied migration — write a new one
  • Existing database: db pull, then baseline with migrate diff --from-empty and migrate resolve --applied 0_init