Database Migrations Guide¶
Complete guide to managing database schema changes in Changemaker Lite V2 using Prisma Migrate and Drizzle Kit.
Overview¶
Changemaker Lite V2 uses two ORMs for different parts of the application:
- Prisma (Main API) - Full-featured ORM with migration tracking
- Drizzle (Media API) - Lightweight ORM with schema push (no migrations)
This guide covers both workflows.
Prisma Migrations (Main API)¶
Migration Workflow Overview¶
1. Edit schema.prisma
↓
2. Create migration (npx prisma migrate dev)
↓
3. Review generated SQL
↓
4. Test migration locally
↓
5. Commit migration files
↓
6. Deploy to production (npx prisma migrate deploy)
Understanding Prisma Migrate¶
Prisma Migrate:
- Tracks schema changes as SQL migration files
- Stores migration history in _prisma_migrations table
- Ensures schema consistency across environments
- Supports rollback via version control
Migration Files:
- Located in api/prisma/migrations/
- Named with timestamp: 20260213123456_description/
- Contains migration.sql (SQL commands)
Migration States: - Pending: Not yet applied - Applied: Successfully executed - Failed: Execution error (requires manual fix)
Creating Migrations¶
Step 1: Edit Prisma Schema¶
Edit api/prisma/schema.prisma:
// Before
model User {
id Int @id @default(autoincrement())
email String @unique
password String
role Role @default(USER)
createdAt DateTime @default(now())
}
// After (add name field)
model User {
id Int @id @default(autoincrement())
email String @unique
password String
name String? // New field (nullable)
role Role @default(USER)
createdAt DateTime @default(now())
}
Step 2: Validate Schema¶
Expected output:
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
The schema is valid ✔
If errors:
Fix errors before proceeding.
Step 3: Create Migration¶
What happens:
1. Prisma detects schema changes
2. Generates SQL migration file
3. Prompts for migration name (or uses --name argument)
4. Applies migration to development database
5. Regenerates Prisma Client
Expected output:
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "changemaker_v2_db"
Applying migration `20260213123456_add_user_name`
Running seed command `tsx prisma/seed.ts` ...
✔ Generated Prisma Client to ./node_modules/@prisma/client
Migration file created:
Step 4: Review Generated SQL¶
Example SQL:
Verify SQL is correct:
- Check table names match expectations
- Ensure data types are correct
- Look for unexpected DROP commands
Step 5: Test Migration¶
Migration already applied to development DB. Verify:
Or query directly:
# PostgreSQL shell
docker compose exec v2-postgres psql -U changemaker_v2 -d changemaker_v2_db
# Describe users table
changemaker_v2_db=# \d users;
Expected output:
Column | Type | Nullable | Default
----------+---------+----------+---------
id | integer | not null | nextval(...)
email | text | not null |
password | text | not null |
name | text | | <-- New field
role | text | not null | 'USER'
created_at| timestamp| not null | now()
Step 6: Commit Migration¶
git add prisma/migrations/20260213123456_add_user_name/
git add prisma/schema.prisma
git commit -m "feat(db): add name field to User model"
Always commit:
- Migration directory (prisma/migrations/*/)
- Updated schema.prisma
Applying Migrations (Production)¶
In Production Environment¶
What it does:
- Checks _prisma_migrations table for applied migrations
- Applies only pending migrations
- Does NOT create new migrations
- Safe for production
Expected output:
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "changemaker_v2_prod_db"
2 migrations found in prisma/migrations
Applying migration `20260213123456_add_user_name`
Applying migration `20260214000000_add_user_avatar`
All migrations have been successfully applied.
In Docker¶
# Apply migrations in Docker container
docker compose exec api npx prisma migrate deploy
# Or during container startup (Dockerfile)
CMD npx prisma migrate deploy && npm start
CI/CD Deployment¶
Migration Best Practices¶
1. Incremental Changes¶
Make small, focused migrations:
Good:
# Separate migrations
npx prisma migrate dev --name add_user_name
npx prisma migrate dev --name add_user_avatar
npx prisma migrate dev --name add_user_bio
Bad:
# One huge migration
npx prisma migrate dev --name update_user_model
# (adds 10 fields, 3 relations, 5 indexes)
2. Descriptive Names¶
Use clear migration names:
Good:
npx prisma migrate dev --name add_user_name
npx prisma migrate dev --name make_email_unique
npx prisma migrate dev --name create_posts_table
npx prisma migrate dev --name add_user_posts_relation
Bad:
npx prisma migrate dev --name update
npx prisma migrate dev --name fix
npx prisma migrate dev --name changes
3. Review SQL Before Committing¶
Always review generated SQL:
Watch for:
- Unexpected DROP TABLE or DROP COLUMN
- Missing NOT NULL constraints
- Incorrect data types
- Missing indexes on foreign keys
4. Backup Before Migration (Production)¶
# Backup database before deploy
docker compose exec v2-postgres pg_dump -U changemaker_v2 changemaker_v2_db > backup-$(date +%Y%m%d).sql
# Apply migration
npx prisma migrate deploy
# If migration fails, restore:
cat backup-20260213.sql | docker compose exec -T v2-postgres psql -U changemaker_v2 changemaker_v2_db
5. Test on Staging First¶
Never deploy migrations directly to production:
1. Create migration in development
2. Test locally
3. Commit to version control
4. Deploy to staging environment
5. Test on staging
6. Deploy to production
Common Migration Scenarios¶
Add New Field¶
// schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String? // New nullable field
}
Generated SQL:
Add Required Field (with Default)¶
model User {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now()) // New required field with default
}
Generated SQL:
Add New Table¶
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
}
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
Generated SQL:
CREATE TABLE "posts" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT,
"published" BOOLEAN NOT NULL DEFAULT false,
"author_id" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "posts_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "posts_author_id_idx" ON "posts"("author_id");
ALTER TABLE "posts" ADD CONSTRAINT "posts_author_id_fkey"
FOREIGN KEY ("author_id") REFERENCES "users"("id")
ON DELETE RESTRICT ON UPDATE CASCADE;
Add Relation¶
model Campaign {
id Int @id @default(autoincrement())
title String
createdByUserId Int // New foreign key
createdBy User @relation(fields: [createdByUserId], references: [id])
}
model User {
id Int @id @default(autoincrement())
email String @unique
campaigns Campaign[]
}
Generated SQL:
ALTER TABLE "campaigns" ADD COLUMN "created_by_user_id" INTEGER NOT NULL;
CREATE INDEX "campaigns_created_by_user_id_idx" ON "campaigns"("created_by_user_id");
ALTER TABLE "campaigns" ADD CONSTRAINT "campaigns_created_by_user_id_fkey"
FOREIGN KEY ("created_by_user_id") REFERENCES "users"("id")
ON DELETE RESTRICT ON UPDATE CASCADE;
Change Field Type¶
Generated SQL:
Warning: This may fail if data cannot be cast. Consider data migration first.
Add Unique Constraint¶
Generated SQL:
Add Index¶
Generated SQL:
Migration History and Status¶
Check Migration Status¶
Expected output:
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "changemaker_v2_db"
Database schema is up to date!
Following migrations have been applied:
20260101000000_init
20260105000000_add_campaigns
20260110000000_add_locations
20260213123456_add_user_name
View Migration History¶
# List migration files
ls -la api/prisma/migrations/
# View specific migration
cat api/prisma/migrations/20260213123456_add_user_name/migration.sql
Check Database Migration Table¶
docker compose exec v2-postgres psql -U changemaker_v2 -d changemaker_v2_db -c "SELECT * FROM _prisma_migrations;"
Output:
id | checksum | finished_at | migration_name | logs
---+----------+-------------+----------------+-----
1 | abc123 | 2026-01-01 | 20260101000000_init | NULL
2 | def456 | 2026-01-05 | 20260105000000_add_campaigns | NULL
Rollback Strategies¶
Prisma Migrate does NOT have automatic rollback. Use these strategies:
1. Version Control Rollback¶
# Revert schema changes
git revert <commit-hash>
# Create new migration to undo changes
npx prisma migrate dev --name revert_user_name
# This creates a new migration that undoes the previous one
2. Manual Rollback Migration¶
Create a new migration to reverse changes:
// If you added a field, remove it
model User {
id Int @id @default(autoincrement())
email String @unique
// name String? // Remove this
}
Generated SQL:
3. Database Restore (Last Resort)¶
# Restore from backup
cat backup-20260213.sql | docker compose exec -T v2-postgres psql -U changemaker_v2 changemaker_v2_db
# Mark migrations as rolled back
docker compose exec v2-postgres psql -U changemaker_v2 -d changemaker_v2_db -c "
DELETE FROM _prisma_migrations
WHERE migration_name = '20260213123456_add_user_name';
"
4. Reset Development Database¶
WARNING: Deletes all data!
This: 1. Drops all tables 2. Re-applies all migrations from scratch 3. Runs seed script
Handling Migration Conflicts¶
Schema Drift¶
Problem: Database schema doesn't match Prisma schema.
Symptoms:
Solution:
# Check what's different
npx prisma migrate diff \
--from-schema-datamodel prisma/schema.prisma \
--to-schema-datasource prisma/schema.prisma
# Create migration to fix drift
npx prisma migrate dev --name fix_schema_drift
Failed Migration¶
Problem: Migration fails during apply.
Symptoms:
Error: Migration failed with error:
ALTER TABLE "users" ADD COLUMN "age" INTEGER NOT NULL;
ERROR: column "age" contains null values
Solution:
# 1. Mark migration as rolled back
npx prisma migrate resolve --rolled-back 20260213123456_add_user_age
# 2. Fix migration SQL manually
vi prisma/migrations/20260213123456_add_user_age/migration.sql
# Change to:
ALTER TABLE "users" ADD COLUMN "age" INTEGER; -- Make nullable first
UPDATE "users" SET "age" = 0 WHERE "age" IS NULL; -- Set default
ALTER TABLE "users" ALTER COLUMN "age" SET NOT NULL; -- Then make required
# 3. Apply migration again
npx prisma migrate deploy
Conflicting Migrations (Team Environment)¶
Problem: Two developers create migrations simultaneously.
Solution:
# 1. Pull latest changes
git pull origin v2
# 2. Prisma detects conflict
npx prisma migrate dev
# 3. Resolve by creating merge migration
# Prisma will prompt you to create a migration that includes both changes
Data Migrations¶
Prisma Migrate handles schema changes, not data changes. For data transformations:
Option 1: Custom SQL in Migration¶
Edit generated migration file:
-- Add column (Prisma-generated)
ALTER TABLE "users" ADD COLUMN "full_name" TEXT;
-- Populate from existing data (manual addition)
UPDATE "users" SET "full_name" = "first_name" || ' ' || "last_name";
-- Remove old columns (Prisma-generated)
ALTER TABLE "users" DROP COLUMN "first_name";
ALTER TABLE "users" DROP COLUMN "last_name";
Option 2: Separate Data Migration Script¶
// api/prisma/data-migrations/20260213-populate-full-name.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const users = await prisma.user.findMany();
for (const user of users) {
await prisma.user.update({
where: { id: user.id },
data: {
fullName: `${user.firstName} ${user.lastName}`
}
});
}
console.log(`Updated ${users.length} users`);
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
Run after migration:
Drizzle Push (Media API)¶
Drizzle Overview¶
Drizzle Kit Push: - Syncs schema directly to database - No migration files generated - Fast iteration for prototyping - Used only for Media API tables
Schema Location:
- api/src/modules/media/db/schema.ts
When to Use: - Rapid prototyping - Development only - Media API tables (videos, jobs, reactions)
When NOT to Use: - Production deployments - Main API tables (use Prisma) - When migration history is needed
Drizzle Push Workflow¶
Step 1: Edit Schema¶
Edit api/src/modules/media/db/schema.ts:
// Before
export const videos = pgTable('videos', {
id: serial('id').primaryKey(),
filename: text('filename').notNull(),
title: text('title'),
duration: integer('duration'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// After (add description field)
export const videos = pgTable('videos', {
id: serial('id').primaryKey(),
filename: text('filename').notNull(),
title: text('title'),
description: text('description'), // New field
duration: integer('duration'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
Step 2: Push Schema¶
Or directly:
What happens: 1. Drizzle compares schema to database 2. Generates SQL for changes 3. Applies changes immediately 4. No migration files created
Expected output:
Reading config from drizzle.config.ts
Using 'pg' driver for database querying
Pulling schema from database...
[✓] Schema pulled successfully
Comparing schemas...
[!] Changes detected:
- ALTER TABLE "videos" ADD COLUMN "description" TEXT;
Do you want to execute these changes? [y/N]: y
Applying changes...
[✓] Schema pushed successfully
Step 3: Verify Changes¶
Or query directly:
Drizzle Best Practices¶
1. Development Only¶
Use Drizzle Push only in development:
Good:
Bad:
2. Backup Before Push¶
Always backup before pushing schema:
# Backup database
docker compose exec v2-postgres pg_dump -U changemaker_v2 changemaker_v2_db > backup.sql
# Push schema
npm run drizzle:push
# If something breaks, restore:
cat backup.sql | docker compose exec -T v2-postgres psql -U changemaker_v2 changemaker_v2_db
3. Test Changes Locally¶
Never push untested schema changes:
# 1. Edit schema
vi src/modules/media/db/schema.ts
# 2. Push to dev database
npm run drizzle:push
# 3. Test with Drizzle Studio
npm run drizzle:studio
# 4. Test API endpoints
curl http://localhost:4100/api/media/videos
Drizzle vs Prisma¶
| Feature | Prisma Migrate | Drizzle Push |
|---|---|---|
| Migration files | ✅ Yes | ❌ No |
| Migration history | ✅ Tracked | ❌ Not tracked |
| Rollback | ✅ Via version control | ❌ Manual only |
| Production use | ✅ Recommended | ⚠️ Not recommended |
| Prototyping | ⚠️ Slower | ✅ Faster |
| Use case | Main API tables | Media API tables |
Seeding After Migration¶
Running Seed Script¶
After migrations, seed database:
What it does:
- Runs prisma/seed.ts
- Creates admin user
- Creates default settings
- Creates sample blocks
Expected output:
Running seed command `tsx prisma/seed.ts` ...
Seeding database...
Created default settings
Created admin user: admin@example.com
Created 10 sample blocks
Seed completed successfully
Custom Seed Data¶
Edit api/prisma/seed.ts:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Create admin user
await prisma.user.upsert({
where: { email: 'admin@example.com' },
update: {},
create: {
email: 'admin@example.com',
password: await hashPassword('Admin123!'),
role: 'SUPER_ADMIN',
name: 'Admin User'
}
});
// Create sample campaign
await prisma.campaign.create({
data: {
title: 'Sample Campaign',
description: 'This is a sample campaign',
active: true,
createdByUserId: 1
}
});
console.log('Seed completed');
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
CI/CD Integration¶
GitHub Actions Example¶
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
working-directory: ./api
run: npm ci
- name: Run migrations
working-directory: ./api
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npx prisma migrate deploy
- name: Seed database
working-directory: ./api
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npx prisma db seed
Docker Deployment¶
# api/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
# Generate Prisma Client
RUN npx prisma generate
# Run migrations on startup
CMD npx prisma migrate deploy && npm start
Troubleshooting¶
Migration Fails with "Column Already Exists"¶
Problem:
Solution:
# Mark migration as applied
npx prisma migrate resolve --applied 20260213123456_add_user_name
# Or drop column manually and re-run
docker compose exec v2-postgres psql -U changemaker_v2 -d changemaker_v2_db -c "ALTER TABLE users DROP COLUMN name;"
npx prisma migrate deploy
Migration Fails with "Relation Does Not Exist"¶
Problem:
Solution:
# Check migration history
npx prisma migrate status
# Apply missing migrations
npx prisma migrate deploy
# Or reset (development only)
npx prisma migrate reset
Schema Out of Sync¶
Problem:
Solution:
# Generate migration to fix drift
npx prisma migrate dev --name fix_drift
# Or in production, create explicit migration
npx prisma migrate diff \
--from-schema-datamodel prisma/schema.prisma \
--to-schema-datasource prisma/schema.prisma \
--script > fix-drift.sql
# Review fix-drift.sql and apply manually
Drizzle Push Fails¶
Problem:
Solution:
# Check Drizzle config
cat api/drizzle.config.ts
# Verify DATABASE_URL
echo $DATABASE_URL
# Test database connection
docker compose exec v2-postgres psql -U changemaker_v2 -d changemaker_v2_db -c "SELECT 1"
# Clear Drizzle cache and retry
rm -rf api/.drizzle
npm run drizzle:push
Related Documentation¶
- Setup: Local Development Setup
- Commands: NPM Commands Reference
- Docker: Docker Workflow
- Database: Database Schema
- Deployment: Production Deployment
Summary¶
You now know: - ✅ How Prisma Migrate tracks schema changes - ✅ How to create and apply migrations - ✅ Common migration scenarios (add field, table, relation) - ✅ Migration best practices - ✅ How to handle migration conflicts - ✅ How to perform data migrations - ✅ How Drizzle Push works for Media API - ✅ When to use Prisma vs Drizzle - ✅ How to seed database after migrations - ✅ How to integrate migrations in CI/CD
Quick Reference:
# Prisma: Create migration
npx prisma migrate dev --name description
# Prisma: Apply migrations (production)
npx prisma migrate deploy
# Prisma: Check status
npx prisma migrate status
# Drizzle: Push schema (dev only)
npx drizzle-kit push
# Seed database
npx prisma db seed
# Reset (dev only, DELETES DATA)
npx prisma migrate reset