Migrations: Changing the Schema Safely
Your schema will change — you'll add a column, rename a table, add an index. Think of it like tracked edits to a shared document: every change is recorded in order, so everyone's copy can be brought up to date the same way. A migration is a versioned, repeatable script that describes one such change. Migrations matter because the schema in your laptop, your teammate's laptop, and production must stay identical.
The rules that keep migrations safe:
- Never edit the by hand in production. Write a migration.
- Migrations move forward. Each one is a new file; you don't rewrite old ones that already ran.
- Make destructive changes carefully. Dropping a column or table deletes data permanently. Have a backup first.
- Additive changes are safest. Adding a nullable column rarely breaks anything; renaming or removing one can break running code.
Migrations form an ordered chain: each one is a numbered step that moves every copy of the database from one known state to the next, so your laptop, a teammate's, and production all end up identical:
empty DB
│
▼
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 001_init │ ▶ │ 002_add │ ▶ │ 003_index │ ▶ │ 004_bio │
│ users │ │ posts │ │ on author │ │ column │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
apply in order ─────────────────────────────────────────▶ current
never edit a step that already ran — add a NEW one
Tools like Prisma, Drizzle, or Rails migrations generate and track these for you. Let them.
A migration is two halves: the change to apply (up) and how to undo it (down). A trustworthy tool generates both, so a bad can roll back cleanly:
-- up: add an optional bio, safe to apply live
ALTER TABLE users ADD COLUMN bio TEXT;
-- down: reverse it
ALTER TABLE users DROP COLUMN bio;
The genuinely dangerous migrations are renames and type changes, because the old code and the new schema briefly disagree. The professional pattern is to split them into additive steps: add the new column, backfill it, switch the code to read it, and only drop the old column in a later deploy once nothing references it. It feels slow, but it's the difference between a smooth release and a 500-page error spike for every user mid-deploy.