9.8 KiB
Drizzle Migrations Workflow for Neon Postgres
This guide shows the complete workflow for managing database migrations with Drizzle ORM and Neon Postgres.
Initial Setup
1. Install Dependencies
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit
2. Create Configuration File
Create drizzle.config.ts in your project root:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './db/schema.ts',
out: './db/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!
}
});
3. Add Scripts to package.json
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"db:push": "drizzle-kit push"
}
}
Schema Definition
Create your schema in db/schema.ts:
import { pgTable, serial, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
avatarUrl: text('avatar_url'),
createdAt: timestamp('created_at').defaultNow(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').default(false),
createdAt: timestamp('created_at').defaultNow(),
});
export const comments = pgTable('comments', {
id: serial('id').primaryKey(),
postId: integer('post_id').notNull().references(() => posts.id, { onDelete: 'cascade' }),
userId: integer('user_id').notNull().references(() => users.id),
content: text('content').notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
Migration Workflow
Step 1: Generate Migration
After creating or modifying your schema, generate a migration:
npm run db:generate
This creates a new migration file in db/migrations/ directory:
db/migrations/0000_initial.sql- First migrationdb/migrations/0001_add_published_column.sql- Second migration- etc.
What happens:
- Drizzle compares your schema with the database
- Generates SQL migration file with CREATE/ALTER/DROP statements
- Creates snapshot in
db/migrations/meta/for future comparisons
Step 2: Review Migration
ALWAYS review generated SQL before applying!
Example generated migration (db/migrations/0000_initial.sql):
CREATE TABLE IF NOT EXISTS "users" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL UNIQUE,
"avatar_url" text,
"created_at" timestamp DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS "posts" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"title" text NOT NULL,
"content" text,
"published" boolean DEFAULT false,
"created_at" timestamp DEFAULT NOW(),
FOREIGN KEY ("user_id") REFERENCES "users"("id")
);
CREATE TABLE IF NOT EXISTS "comments" (
"id" serial PRIMARY KEY NOT NULL,
"post_id" integer NOT NULL,
"user_id" integer NOT NULL,
"content" text NOT NULL,
"created_at" timestamp DEFAULT NOW(),
FOREIGN KEY ("post_id") REFERENCES "posts"("id") ON DELETE CASCADE,
FOREIGN KEY ("user_id") REFERENCES "users"("id")
);
Step 3: Apply Migration
Apply the migration to your database:
npm run db:migrate
What happens:
- Drizzle connects to your Neon database
- Runs all pending migrations in order
- Records applied migrations in
__drizzle_migrationstable
Output:
✅ Applying migration: 0000_initial.sql
✅ Migration applied successfully
Step 4: Verify Migration
Use Drizzle Studio to verify your schema:
npm run db:studio
Opens a web UI at https://local.drizzle.studio where you can:
- View tables and relationships
- Run queries
- Edit data
Common Scenarios
Scenario 1: Add New Column
- Update schema (
db/schema.ts):
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
avatarUrl: text('avatar_url'),
bio: text('bio'), // ← New column
createdAt: timestamp('created_at').defaultNow(),
});
- Generate migration:
npm run db:generate
Generated SQL (db/migrations/0001_add_bio.sql):
ALTER TABLE "users" ADD COLUMN "bio" text;
- Apply migration:
npm run db:migrate
Scenario 2: Add Index
- Update schema with index:
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').default(false),
createdAt: timestamp('created_at').defaultNow(),
}, (table) => ({
userIdIdx: index('user_id_idx').on(table.userId),
createdAtIdx: index('created_at_idx').on(table.createdAt),
}));
- Generate and apply:
npm run db:generate
npm run db:migrate
Scenario 3: Rename Column
⚠️ WARNING: Drizzle treats renames as DROP + ADD (data loss!)
Safe approach:
- Add new column
- Migrate data (manually)
- Drop old column
Example:
// Step 1: Add new column
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(), // Old
fullName: text('full_name').notNull(), // New
email: text('email').notNull().unique(),
});
npm run db:generate
npm run db:migrate
-- Step 2: Migrate data manually
UPDATE users SET full_name = name WHERE full_name IS NULL;
// Step 3: Remove old column
export const users = pgTable('users', {
id: serial('id').primaryKey(),
fullName: text('full_name').notNull(),
email: text('email').notNull().unique(),
});
npm run db:generate
npm run db:migrate
Scenario 4: Production Deployment
For production, use a separate migration script:
Create scripts/migrate.ts:
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import { migrate } from 'drizzle-orm/neon-http/migrator';
async function runMigrations() {
const sql = neon(process.env.DATABASE_URL!);
const db = drizzle(sql);
console.log('Running migrations...');
await migrate(db, { migrationsFolder: './db/migrations' });
console.log('Migrations completed!');
process.exit(0);
}
runMigrations().catch((err) => {
console.error('Migration failed:', err);
process.exit(1);
});
Add to package.json:
{
"scripts": {
"db:migrate:prod": "tsx scripts/migrate.ts"
}
}
Run in CI/CD:
# Before deployment
npm run db:migrate:prod
Alternative: Push (Schema Sync)
For development only, you can use db:push to sync schema without migrations:
npm run db:push
What it does:
- Compares schema with database
- Applies changes directly
- No migration files created
When to use:
- ✅ Local development (rapid iteration)
- ✅ Prototyping
When NOT to use:
- ❌ Production
- ❌ Shared databases
- ❌ Need migration history
Best Practices
1. Always Review Generated SQL
# Generate migration
npm run db:generate
# Review file before applying
cat db/migrations/0001_*.sql
# Apply only if safe
npm run db:migrate
2. Test Migrations on Development Database First
# Create Neon branch for testing
neonctl branches create --name test-migration --parent main
# Get branch connection string
export DATABASE_URL=$(neonctl connection-string test-migration)
# Test migration
npm run db:migrate
# If successful, apply to main
neonctl branches delete test-migration
export DATABASE_URL=$(neonctl connection-string main)
npm run db:migrate
3. Commit Migration Files to Git
git add db/migrations/
git add db/schema.ts
git commit -m "feat: add bio column to users table"
4. Use Transactions for Multi-Step Migrations
Drizzle migrations run in transactions by default. If any step fails, all changes roll back.
5. Handle Data Migrations Separately
For complex data transformations, create custom migration scripts:
// db/migrations/custom/0001_migrate_user_data.ts
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
// Custom data migration
await sql`UPDATE users SET status = 'active' WHERE status IS NULL`;
Troubleshooting
Problem: "No schema changes detected"
Solution: Drizzle uses snapshots. If out of sync:
# Delete snapshot
rm -rf db/migrations/meta
# Regenerate
npm run db:generate
Problem: "Relation already exists"
Solution: Migration already applied. Check __drizzle_migrations table:
SELECT * FROM __drizzle_migrations;
Problem: "Cannot drop column with data"
Solution: Set column nullable first, then drop:
ALTER TABLE users ALTER COLUMN old_column DROP NOT NULL;
ALTER TABLE users DROP COLUMN old_column;
Complete Checklist
drizzle.config.tsconfigured- Schema defined in
db/schema.ts - Scripts added to
package.json DATABASE_URLenvironment variable set- Initial migration generated (
npm run db:generate) - Migration reviewed
- Migration applied (
npm run db:migrate) - Schema verified in Drizzle Studio
- Migration files committed to git
Resources
- Drizzle ORM Docs: https://orm.drizzle.team/docs/overview
- Drizzle Kit Docs: https://orm.drizzle.team/docs/kit-overview
- Neon + Drizzle Guide: https://orm.drizzle.team/docs/quick-postgresql/neon
- Migration Best Practices: https://orm.drizzle.team/docs/migrations