Database Migrations
Your schema changes over time — new columns, new tables, renamed fields, new indexes. A migration is a versioned, scripted change to the database schema, checked into source control alongside your code. Migrations make schema evolution reproducible: the same ordered set of changes applied to dev, staging, and production yields identical structure. This page explains why migrations matter, the up/down model, and the rules for changing a live database without downtime or data loss.
Why migrations, not manual changes
Reproducible — every environment gets the exact same schema in the same order.
Version-controlled — schema changes are reviewed in PRs, like code.
Collaborative — teammates run pending migrations to sync their local DB.
Reversible — a
downstep can undo a change when a deploy goes wrong.Auditable — the migration history is a timeline of how the schema got here.
The up / down model
migrations/20260620_add_user_role.js
export async function up(db) {
// Apply the change:
await db.query(`
ALTER TABLE users
ADD COLUMN role VARCHAR(20) NOT NULL DEFAULT 'user'
`)
await db.query('CREATE INDEX idx_users_role ON users(role)')
}
export async function down(db) {
// Reverse it (for rollback):
await db.query('DROP INDEX idx_users_role')
await db.query('ALTER TABLE users DROP COLUMN role')
}Tooling per stack
Stack | Migration tool / command |
|---|---|
| |
| |
| |
Knex / raw SQL |
|
migrate-mongo (data migrations, not schema) |
# Typical workflow (Prisma shown): npx prisma migrate dev --name add_user_role # create + apply in dev npx prisma migrate deploy # apply pending in production
Expand–contract: changing live schemas safely
Renaming a column WITHOUT downtime (e.g. 'username' → 'handle'): 1. EXPAND add 'handle'; deploy code that writes BOTH columns 2. BACKFILL copy existing 'username' values into 'handle' 3. MIGRATE deploy code that reads/writes only 'handle' 4. CONTRACT once nothing uses 'username', drop it in a later migration
Rules for migration files
Forward-only in spirit — once a migration is applied in production, never edit it; write a new one to fix mistakes.
Small and focused — one logical change per migration; easier to review and roll back.
Backfill big tables in batches — a single
UPDATEon millions of rows locks the table; chunk it.Separate schema changes from data migrations when possible.
Test the migration on a production-like copy before deploying.