23 KiB
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_migrationstable - 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
cd api
npx prisma validate
Expected output:
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
The schema is valid ✔
If errors:
Error validating model "User": Field "foo" references unknown model "Bar"
Fix errors before proceeding.
Step 3: Create Migration
cd api
npx prisma migrate dev --name add_user_name
What happens:
- Prisma detects schema changes
- Generates SQL migration file
- Prompts for migration name (or uses
--nameargument) - Applies migration to development database
- 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:
api/prisma/migrations/
└── 20260213123456_add_user_name/
└── migration.sql
Step 4: Review Generated SQL
cd api
cat prisma/migrations/20260213123456_add_user_name/migration.sql
Example SQL:
-- AlterTable
ALTER TABLE "users" ADD COLUMN "name" TEXT;
Verify SQL is correct:
- Check table names match expectations
- Ensure data types are correct
- Look for unexpected
DROPcommands
Step 5: Test Migration
Migration already applied to development DB. Verify:
# Check schema with Prisma Studio
cd api
npx prisma studio
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
cd api
npx prisma migrate deploy
What it does:
- Checks
_prisma_migrationstable 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
# GitHub Actions example
- name: Run migrations
run: |
cd api
npx prisma migrate deploy
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:
cat prisma/migrations/*/migration.sql
Watch for:
- Unexpected
DROP TABLEorDROP COLUMN - Missing
NOT NULLconstraints - 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
}
npx prisma migrate dev --name add_user_name
Generated SQL:
ALTER TABLE "users" ADD COLUMN "name" TEXT;
Add Required Field (with Default)
model User {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now()) // New required field with default
}
npx prisma migrate dev --name add_created_at
Generated SQL:
ALTER TABLE "users" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
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[]
}
npx prisma migrate dev --name create_posts_table
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[]
}
npx prisma migrate dev --name add_campaign_user_relation
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
// Before
model User {
age Int
}
// After
model User {
age String // Changed from Int to String
}
npx prisma migrate dev --name change_user_age_to_string
Generated SQL:
ALTER TABLE "users" ALTER COLUMN "age" SET DATA TYPE TEXT;
Warning: This may fail if data cannot be cast. Consider data migration first.
Add Unique Constraint
model User {
email String @unique // Add unique constraint
}
npx prisma migrate dev --name make_email_unique
Generated SQL:
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
Add Index
model User {
email String
@@index([email]) // Add index
}
npx prisma migrate dev --name add_email_index
Generated SQL:
CREATE INDEX "users_email_idx" ON "users"("email");
Migration History and Status
Check Migration Status
cd api
npx prisma migrate 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
}
npx prisma migrate dev --name remove_user_name
Generated SQL:
ALTER TABLE "users" DROP COLUMN "name";
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!
cd api
npx prisma migrate reset
This:
- Drops all tables
- Re-applies all migrations from scratch
- Runs seed script
Handling Migration Conflicts
Schema Drift
Problem: Database schema doesn't match Prisma schema.
Symptoms:
Error: Database schema is not in sync with the migration history
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:
npx tsx prisma/data-migrations/20260213-populate-full-name.ts
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
cd api
npm run drizzle:push
Or directly:
cd api
npx drizzle-kit push
What happens:
- Drizzle compares schema to database
- Generates SQL for changes
- Applies changes immediately
- 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
# Check with Drizzle Studio
cd api
npx drizzle-kit studio
Or query directly:
docker compose exec v2-postgres psql -U changemaker_v2 -d changemaker_v2_db -c "\d videos"
Drizzle Best Practices
1. Development Only
Use Drizzle Push only in development:
Good:
# Development
npm run drizzle:push
Bad:
# Production (use Prisma migrate for production schema changes)
npm run drizzle:push
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:
cd api
npx prisma db seed
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:
Error: column "name" of relation "users" already exists
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:
Error: relation "posts" does not exist
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:
Error: Database schema is not in sync
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:
Error: Could not push schema
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