Database schema migration patterns with Prisma and Drizzle ORM. Covers migration strategies, rollbacks, data migrations, and production deployment.
Manage database schema changes safely with migration tools and best practices.
# Initialize Prisma
npx prisma init
# Create migration from schema changes
npx prisma migrate dev --name init
# Apply migrations in production
npx prisma migrate deploy
# Reset database (dev only)
npx prisma migrate reset
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
@@index([authorId])
}
# 1. Make schema changes in schema.prisma
# 2. Create migration (development)
npx prisma migrate dev --name add_user_role
# 3. Review generated SQL
cat prisma/migrations/*/migration.sql
# 4. Test in staging
DATABASE_URL=$STAGING_URL npx prisma migrate deploy
# 5. Deploy to production
DATABASE_URL=$PROD_URL npx prisma migrate deploy
// prisma/migrations/scripts/backfill-user-roles.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Backfill default role for existing users
await prisma.user.updateMany({
where: { role: null },
data: { role: 'user' },
});
console.log('Backfill complete');
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
// drizzle.config.ts
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema.ts',
out: './drizzle',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
} satisfies Config;
// src/db/schema.ts
import { pgTable, text, timestamp, boolean, index } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text('email').notNull().unique(),
name: text('name'),
role: text('role').default('user'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
}, (table) => ({
emailIdx: index('email_idx').on(table.email),
}));
export const posts = pgTable('posts', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').default(false),
authorId: text('author_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
});
# Generate migration
npx drizzle-kit generate:pg
# Push schema (dev only, no migration files)
npx drizzle-kit push:pg
# Apply migrations
npx drizzle-kit migrate
-- SAFE: Add nullable column
ALTER TABLE users ADD COLUMN phone TEXT;
-- SAFE: Add column with default
ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user';
-- RISKY: Add NOT NULL without default (locks table, may fail)
-- ALTER TABLE users ADD COLUMN required_field TEXT NOT NULL;
-- Step 1: Add new column (nullable)
ALTER TABLE users ADD COLUMN new_email TEXT;
-- Step 2: Backfill data (separate migration)
UPDATE users SET new_email = email;
-- Step 3: Make non-null, add constraint
ALTER TABLE users ALTER COLUMN new_email SET NOT NULL;
ALTER TABLE users ADD CONSTRAINT users_new_email_unique UNIQUE (new_email);
-- Step 4: Drop old column (after app updated)
ALTER TABLE users DROP COLUMN email;
ALTER TABLE users RENAME COLUMN new_email TO email;
// 1. Expand: Add new column/table
// 2. Migrate: Dual-write to old and new
// 3. Contract: Remove old column/table
// During expand phase - dual write
async function createUser(data: UserInput) {
return prisma.user.create({
data: {
...data,
// Write to both old and new columns
email: data.email,
emailNew: data.email,
},
});
}
-- prisma/migrations/rollback/20260123_add_role.sql
ALTER TABLE users DROP COLUMN IF EXISTS role;
# Mark migration as rolled back (doesn't run SQL)
npx prisma migrate resolve --rolled-back 20260123000000_add_role
# Then manually apply rollback SQL
psql $DATABASE_URL -f prisma/migrations/rollback/20260123_add_role.sql
# .github/workflows/deploy.yml
deploy:
steps:
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Deploy app
run: vercel --prod
❌ Running migrate dev in production
❌ Destructive changes without backup
❌ Large data migrations in single transaction
❌ Dropping columns before app update
❌ No rollback plan
✅ Use migrate deploy in production
✅ Always backup before migration
✅ Batch large data migrations
✅ Expand-migrate-contract pattern
✅ Test rollback procedure
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer