Menu
Platform
AI
App store purchases
Credits
Database
Flags
Generated Assets
Monitoring
Notifications
Organizations
Payments
- Payments
- Payments quickstart
- Checkout policies
- Checkout sessions
- Customer subscriptions
- Entitlement grants
- Exchange rates
- Licence keys
- Licence tokens
- Merchant accounts
- Portal sessions
- Price catalogs
- Revenue
- Revenue facts
- Revenue sources
- Tax threshold statuses
- Usage events
- Usage invoices
- Usage meter prices
- Usage meters
- Usage reservations
- Usage settings
- Usage tiers
Queues
Realtime
Sandboxes
Webhooks
Getting Started
Authentication
KV Store
Deploy & Infrastructure
Reference
Migrations
How schema changes reach a database, over the direct connection, without a window of downtime.
Migrations run over the direct_uri that connect returns, never over the
pooled one: a migration needs a session of its own, and the pooler will not
keep one for it.
The connection migrations use
connect answers with uri (pooled) and direct_uri (direct). Point the
migration tool at direct_uri; point the application at uri. The quickstart
shows the call.
#Migration workflow
Drizzle Kit generates SQL from your schema and applies it in order:
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './src/lib/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
// The direct connection, not the pooled one
url: process.env.DIRECT_DATABASE_URL!,
},
// Optional: customize migration table name
migrations: {
table: '__drizzle_migrations',
schema: 'public',
},
})#Generate a migration
Change the schema:
// src/lib/db/schema.ts
import { pgTable, text, timestamp, uuid, boolean } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
name: text('name'),
// NEW: Add a bio field
bio: text('bio'),
// NEW: Add email verification
emailVerified: boolean('email_verified').default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
})Generate the SQL, then read it before you apply it:
# Generate migration SQL
npx drizzle-kit generate
# Output:
# [✓] Generated migration: 0001_add_bio_and_email_verified.sql-- Generated by Drizzle Kit
ALTER TABLE "users" ADD COLUMN "bio" text;
ALTER TABLE "users" ADD COLUMN "email_verified" boolean DEFAULT false;# Apply pending migrations
npx drizzle-kit migrate
# Output:
# [✓] Applied migration: 0001_add_bio_and_email_verified.sql
# [✓] Database schema is up to date#Development workflow
While the schema is still moving, push it straight at a development database:
# Quick sync schema to database (development only!)
npx drizzle-kit push
# This directly applies schema changes without creating migration files
# WARNING: Don't use in production - changes aren't tracked
# Open Drizzle Studio to browse data
npx drizzle-kit studio
# Opens at https://local.drizzle.studioPush or migrate
push changes the database without writing down what it did, so nothing
records how it got that way and no other environment can follow. It is for a
development database you can throw away. Every shared environment takes
generated migrations.
#Roll back a migration
Drizzle Kit has no automatic rollback. A rollback is a new migration that reverses the old one, and in production that is the only rollback you want: the schema's history stays append-only, and every environment replays the same statements.
// Option 1: Create a new migration that reverts changes
// 1. Modify schema to remove the problematic changes
// 2. Run: npx drizzle-kit generate
// 3. This creates a migration that reverses the previous one
// Option 2: Manual SQL rollback
// Create a file: drizzle/0002_rollback_bio.sql
/*
ALTER TABLE "users" DROP COLUMN "bio";
ALTER TABLE "users" DROP COLUMN "email_verified";
*/When the migration itself was the mistake — it backfilled wrongly, or a
constraint is already rejecting writes — the faster path is a restore to a
point before it ran. A restore takes the database back to a point in time
inside its retention window, in place. The restore call is not served on the
one API; write to hi@sylphx.com with the database name and the moment you
want it back to.
A restore is not a rollback
It discards everything written after the restore point, including writes that have nothing to do with the migration. Reverse with a migration when you can; restore when the data is already wrong. Backups and restore has the full procedure.
#Zero-downtime migrations
A migration that takes a lock on a table blocks every query against it for as long as it holds. The safe shapes:
-- Safe: Adding a nullable column
ALTER TABLE "users" ADD COLUMN "bio" text;
-- Safe: Adding a column with default
ALTER TABLE "users" ADD COLUMN "status" text DEFAULT 'active';
-- Note: In PostgreSQL 11+, adding columns with defaults
-- doesn't rewrite the table, making it fast and safe// WRONG: Removing a column directly breaks running code
// ALTER TABLE "users" DROP COLUMN "old_field"; // DON'T DO THIS
// RIGHT: Three-phase deployment
// Phase 1: Deploy code that stops WRITING to old_field
// (old_field still exists in DB, reads still work)
// Phase 2: Deploy code that stops READING old_field
// (code no longer depends on old_field)
// Phase 3: Drop the column
// ALTER TABLE "users" DROP COLUMN "old_field";// WRONG: Direct rename breaks running code
// ALTER TABLE "users" RENAME COLUMN "name" TO "full_name"; // DON'T DO THIS
// RIGHT: Add new, migrate data, remove old
// Phase 1: Add new column, write to both
/*
ALTER TABLE "users" ADD COLUMN "full_name" text;
UPDATE "users" SET "full_name" = "name";
*/
// Deploy code that writes to BOTH columns
// Phase 2: Backfill and switch reads
// Deploy code that reads from "full_name"
// Phase 3: Stop writing to old column
// Phase 4: Drop old column
/*
ALTER TABLE "users" DROP COLUMN "name";
*/The reason for the phases is that during a rollout, old and new code run at the same time. A schema change every version of the code can survive is one that never needs a maintenance window.
#Add an index without locking
CREATE INDEX holds a write lock on the table while it builds.
CREATE INDEX CONCURRENTLY does not, at the cost of taking longer:
-- Standard index creation (BLOCKS writes during creation)
-- CREATE INDEX "posts_title_idx" ON "posts" ("title");
-- Concurrent index creation (NO blocking, but slower)
CREATE INDEX CONCURRENTLY "posts_title_idx" ON "posts" ("title");
-- Note: CONCURRENTLY can't run inside a transaction
-- Drizzle migrations run in transactions by default
-- You may need to run this manually or configure migration behaviorimport { db } from '@/lib/db'
import { sql } from 'drizzle-orm'
// For large tables, create indexes outside of migrations
async function createSearchIndex() {
// This runs outside a transaction
await db.execute(sql`
CREATE INDEX CONCURRENTLY IF NOT EXISTS "posts_search_idx"
ON "posts" USING gin(to_tsvector('english', "title" || ' ' || "content"))
`)
}
// Run this in a background job or deployment scriptConcurrent index caveats
A concurrent build that fails leaves an invalid index behind: writes keep
paying for it and the planner will not use it. Drop it by name and build
again. Check for them with
SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid.
#Integrate with CI/CD
Migrations belong in the deploy, before the code that needs them:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run migrations
env:
DIRECT_DATABASE_URL: ${{ secrets.DIRECT_DATABASE_URL }}
run: npx drizzle-kit migrate
- name: Deploy application
run: npm run deployAnd a check on the pull request, so a schema change cannot merge without its migration:
name: Check Migrations
on:
pull_request:
paths:
- 'src/lib/db/schema.ts'
- 'drizzle/**'
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Check migration consistency
run: npx drizzle-kit check
- name: Verify schema matches migrations
run: |
npx drizzle-kit generate --dry-run
# Fails if there are ungenerated schema changes#Best practices
- Add nullable columns and new tables first: old code has to work against the new schema, because during a rollout it will be running against it.
- Rehearse on a database that is not production. A restore gives you one at the current data, and lets you throw the rehearsal away.
- For a breaking change: add the new thing, migrate the data, move the code, then remove the old thing — four deploys, each of which can be rolled back.
- One logical change per migration. It is easier to review, and when it fails it is obvious which part failed.
- Once a migration has run anywhere shared, it is history: change it with a new migration, never by editing the file.
- Apply the migration before the code that depends on it, and give the database a moment: the old code must survive the new schema, and it does only if the migration is backward compatible.
#Checklist
// Pre-deployment checklist:
// [ ] Migration tested on database branch
// [ ] Rollback plan documented
// [ ] Backup/snapshot created
// [ ] Off-peak deployment time scheduled (for large tables)
// [ ] Monitoring alerts configured
// [ ] Team notified of deployment
// Post-deployment verification:
// [ ] Migration completed without errors
// [ ] Application health checks passing
// [ ] No increase in error rates
// [ ] Query performance unchanged
// [ ] Rollback plan tested (optional)