How to Set Up Database Migrations (2026)
Database migrations track schema changes over time. Without them, deploying database changes is manual and error-prone. Here's how to set them up properly.
Why Migrations?
Migrations are version control for your database schema:
migration_001_create_users.sql → CREATE TABLE users
migration_002_add_email_column.sql → ALTER TABLE users ADD email
migration_003_create_posts.sql → CREATE TABLE posts
Every developer and environment runs the same migrations in order. Your production database matches development.
Option 1: Drizzle Migrations (Recommended)
Setup
npm install drizzle-orm drizzle-kit
Define Schema
// src/db/schema.ts
import { pgTable, text, uuid, timestamp, boolean } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
createdAt: timestamp('created_at').defaultNow(),
})
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').default(false),
authorId: uuid('author_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
})
Generate Migration
npx drizzle-kit generate
This creates a SQL migration file:
-- drizzle/0000_create_users_and_posts.sql
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"email" text NOT NULL UNIQUE,
"name" text NOT NULL,
"created_at" timestamp DEFAULT now()
);
CREATE TABLE "posts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"title" text NOT NULL,
"content" text,
"published" boolean DEFAULT false,
"author_id" uuid REFERENCES "users"("id"),
"created_at" timestamp DEFAULT now()
);
Run Migration
npx drizzle-kit migrate
Workflow
1. Modify schema.ts
2. Run `drizzle-kit generate` → creates SQL migration
3. Review the generated SQL
4. Run `drizzle-kit migrate` → applies to database
5. Commit schema + migration file to git
Option 2: Prisma Migrations
npm install prisma @prisma/client
Define Schema
// prisma/schema.prisma
model User {
id String @id @default(uuid())
email String @unique
name String
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id String @id @default(uuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
}
Generate and Run
npx prisma migrate dev --name init # Development
npx prisma migrate deploy # Production
Option 3: Raw SQL Migrations (golang-migrate)
For maximum control:
migrate create -ext sql -dir migrations create_users
-- migrations/000001_create_users.up.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- migrations/000001_create_users.down.sql
DROP TABLE users;
migrate -database $DATABASE_URL -path migrations up
Production Migration Best Practices
1. Never Edit Existing Migrations
Once a migration is committed, treat it as immutable. Create a new migration for changes.
2. Test Migrations Before Production
# Create a test database
createdb myapp_test
DATABASE_URL=postgresql://localhost/myapp_test npx drizzle-kit migrate
3. Backward-Compatible Changes
Deploy in steps to avoid downtime:
Adding a column:
Step 1: Add column (nullable)
Step 2: Deploy code that writes to new column
Step 3: Backfill existing rows
Step 4: Make column NOT NULL
Renaming a column:
Step 1: Add new column
Step 2: Deploy code that writes to both columns
Step 3: Migrate data from old to new
Step 4: Deploy code that only uses new column
Step 5: Drop old column
4. Run Migrations in CI
# GitHub Actions
- name: Run migrations
run: npx drizzle-kit migrate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
5. Always Have Rollback Plan
# Drizzle: drop the changes manually or restore backup
# Prisma: prisma migrate resolve
# golang-migrate: migrate down 1
FAQ
Drizzle vs Prisma for migrations?
Drizzle generates SQL you can review and modify. Prisma generates migrations automatically. Drizzle gives more control; Prisma is more automated.
Should I use an ORM for migrations?
Yes for most projects. Raw SQL migrations (golang-migrate) for teams that want maximum control or use multiple languages.
How do I handle data migrations?
Separate schema migrations from data migrations. Run data migrations as scripts, not in the migration system.
Bottom Line
Use Drizzle for TypeScript projects (you review the SQL). Use Prisma for rapid development (more automated). Use golang-migrate for polyglot teams. Always: review migrations before applying, test on staging, deploy backward-compatible changes, and never edit committed migrations.