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
| Command | Where | What it does |
|---|---|---|
| migrate dev | Development only | Creates new migrations from schema changes and applies them; may offer to reset the database |
| migrate dev --create-only | Development | Writes the migration SQL without applying it, so you can edit it first |
| migrate deploy | CI, staging, production | Applies pending migrations from the migrations folder; never creates new ones or resets |
| migrate status | Anywhere | Shows which migrations have and haven't been applied |
| migrate resolve | Recovery | Marks a migration as applied or rolled back after you fix a problem by hand |
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?
| Change | Risk | Safe approach |
|---|---|---|
| Add a table, or an optional column | Low | Just migrate |
| Add a required column | Fails on existing rows unless it has a default | Add with @default, or optional → backfill → required |
| Add an index | Can lock a big table while building | On PostgreSQL, edit the SQL to CREATE INDEX CONCURRENTLY for large tables |
| Rename a column or table | Prisma generates drop + add: data loss | Edit the SQL to a rename, or use @map |
| Change a column's type or meaning | Old code breaks during deploy | Expand and contract |
| Drop a column or table | Irreversible; running code may still use it | Stop 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:
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.
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).
| Step | Schema | Code |
|---|---|---|
| 1. Expand | Add status PostStatus? (optional) | Unchanged |
| 2. Dual write | — | Every write sets both published and status |
| 3. Backfill | A data migration fills status for old rows | — |
| 4. Switch reads | Make status required | Read status; still write both |
| 5. Stop old writes | — | Only write status |
| 6. Contract | Drop 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:
- Write data migrations so they're safe to re-run (
WHERE "status" IS NULL). - On a very large table, one
UPDATEcan lock rows for a long time. Backfill in batches from a script (using Chapter 3's raw SQL orupdateManywith atake-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
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.
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
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:
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
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.
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.
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 solutionChapter 8 Quick Reference
migrate devin development only;migrate deployeverywhere else- Renames:
--create-only, edit toRENAME 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-onlymigration; re-runnable; batch big tables - CI:
migrate diff --from-migrations ... --to-schema ... --exit-code; deploy:migrate deployonce - Never edit an applied migration — write a new one
- Existing database:
db pull, then baseline withmigrate diff --from-emptyandmigrate resolve --applied 0_init