Migration Workflow¶
Overview¶
Changemaker Lite V2 uses a dual ORM architecture with separate migration workflows:
- Prisma Migrate — Main API (Express, 30 models)
- Drizzle Kit — Media API (Fastify, 3 models)
Both ORMs share the same PostgreSQL database but maintain independent migration histories.
Prisma Migration Workflow¶
Development Workflow¶
1. Modify Schema¶
Edit api/prisma/schema.prisma:
model Location {
id String @id @default(cuid())
address String
// Add new field:
province String?
// ...
}
2. Create Migration¶
This command:
- Generates SQL migration file in prisma/migrations/
- Applies migration to database
- Regenerates Prisma Client
- Updates _prisma_migrations table
Output:
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "changemaker_v2", schema "public"
Applying migration `20260213120000_add_province_to_location`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20260213120000_add_province_to_location/
└─ migration.sql
Your database is now in sync with your schema.
3. Review Migration SQL¶
-- migrations/20260213120000_add_province_to_location/migration.sql
-- AlterTable
ALTER TABLE "locations" ADD COLUMN "province" TEXT;
4. Commit Migration¶
Production Workflow¶
1. Deploy Migration¶
This command:
- Applies pending migrations from prisma/migrations/
- Does NOT create new migrations
- Does NOT prompt for confirmations
- Safe for production/CI pipelines
2. Verify Migration Status¶
Output:
1 migration found in prisma/migrations
Following migration have been applied:
20260213120000_add_province_to_location
Database schema is up to date!
Common Migration Scenarios¶
Add Field (Nullable)¶
Migration:Add Field (Required with Default)¶
Migration:Add Relation¶
model Shift {
cutId String?
cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull)
}
ALTER TABLE "shifts" ADD COLUMN "cut_id" TEXT;
CREATE INDEX "shifts_cut_id_idx" ON "shifts"("cut_id");
ALTER TABLE "shifts" ADD CONSTRAINT "shifts_cut_id_fkey" FOREIGN KEY ("cut_id") REFERENCES "cuts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
Change Field Type¶
Migration (requires data migration):-- Step 1: Add new column
ALTER TABLE "locations" ADD COLUMN "geocode_confidence_new" INTEGER;
-- Step 2: Migrate data (custom logic)
UPDATE "locations" SET "geocode_confidence_new" = CAST("geocode_confidence" AS INTEGER)
WHERE "geocode_confidence" ~ '^[0-9]+$';
-- Step 3: Drop old column
ALTER TABLE "locations" DROP COLUMN "geocode_confidence";
-- Step 4: Rename new column
ALTER TABLE "locations" RENAME COLUMN "geocode_confidence_new" TO "geocode_confidence";
Add Enum¶
Migration:Add Index¶
Migration:Migration Commands Reference¶
| Command | Description | Environment |
|---|---|---|
npx prisma migrate dev |
Create + apply migration | Development |
npx prisma migrate deploy |
Apply pending migrations | Production/CI |
npx prisma migrate status |
Check migration status | All |
npx prisma migrate reset |
Reset DB + apply all migrations | Development only |
npx prisma db push |
Push schema without migrations | Prototyping only |
npx prisma studio |
Open Prisma Studio (DB GUI) | Development |
Safe Migration Practices¶
✅ DO¶
- Always review generated SQL before committing
- Test migrations on dev database first
- Back up production database before deploying migrations
- Use nullable fields for new columns on existing tables
- Use
@default()for new required fields - Commit migration files to version control
❌ DON'T¶
- Use
prisma db pushin production (skips migrations) - Use
prisma migrate resetin production (deletes data) - Manually edit migration files after applying
- Delete old migration files (breaks history)
- Change field names without data migration plan
Drizzle Migration Workflow¶
Development Workflow¶
1. Modify Schema¶
Edit api/src/modules/media/db/schema.ts:
export const videos = pgTable('videos', {
id: serial('id').primaryKey(),
path: text('path').notNull().unique(),
// Add new field:
description: text('description'),
// ...
});
2. Push Schema Changes¶
This command: - Generates SQL diff from schema - Applies changes directly to database - Does NOT create migration files (Drizzle push mode) - Updates database schema immediately
Output:
Reading config file '/home/bunker-admin/changemaker.lite/api/drizzle.config.ts'
Pulling schema from database...✓
Applying changes...
[✓] Applying: ALTER TABLE "videos" ADD COLUMN "description" text;
Schema applied successfully!
3. Verify Schema¶
Opens Drizzle Studio athttps://local.drizzle.studio/ for database inspection.
Production Workflow¶
Same as development:
Drizzle vs Prisma Migrate¶
| Feature | Prisma Migrate | Drizzle Kit Push |
|---|---|---|
| Migration files | ✓ Generated | ✗ Not generated |
| Migration history | ✓ Tracked in _prisma_migrations |
✗ No history table |
| Rollback support | ✓ Via migration files | ✗ Manual only |
| Production safety | ✓ Explicit deploy step | ⚠️ Direct push |
| Best for | Main API (schema stability) | Media API (rapid iteration) |
Why Drizzle for Media API? - Smaller schema (3 tables vs 30) - Faster iteration during development - Simpler deployment (no migration history to manage) - Media API is newer (less risk of breaking changes)
Drizzle Commands Reference¶
| Command | Description |
|---|---|
npx drizzle-kit push |
Push schema changes to DB |
npx drizzle-kit studio |
Open Drizzle Studio |
npx drizzle-kit generate |
Generate migrations (not used) |
Migration File Structure¶
Prisma Migrations¶
api/prisma/migrations/
├── 20260211120000_initial/
│ └── migration.sql
├── 20260211125000_add_refresh_tokens/
│ └── migration.sql
├── 20260212100000_add_canvass_system/
│ └── migration.sql
└── migration_lock.toml
File naming: YYYYMMDDHHMMSS_description/migration.sql
migration_lock.toml:
Drizzle Schema (No Migrations)¶
Rollback Strategies¶
Prisma Rollback (Manual)¶
Scenario: Migration 20260213120000_add_province caused issues.
Step 1: Identify last good migration
Step 2: Manually revert migration SQL
Step 3: Mark migration as rolled back
Step 4: Remove migration file
Step 5: Fix schema
Edit prisma/schema.prisma to remove province field.
Step 6: Create new migration
Drizzle Rollback (Manual)¶
Step 1: Revert schema changes in schema.ts
Step 2: Push reverted schema
Step 3: If data loss occurred, restore from backup
Common Migration Errors¶
Error: "Migration failed to apply cleanly"¶
Cause: Database state doesn't match expected state Solution:
npx prisma migrate resolve --applied <migration-name> # Mark as applied
# OR
npx prisma migrate resolve --rolled-back <migration-name> # Mark as rolled back
Error: "Unique constraint violation"¶
Cause: Trying to add unique constraint on column with duplicate values Solution: 1. Clean up duplicate data first 2. Run migration
Error: "Column cannot be NOT NULL"¶
Cause: Trying to add required field to table with existing rows
Solution: Use @default() or make field nullable
Error: "Foreign key constraint failed"¶
Cause: Referencing non-existent records Solution: Ensure related records exist before adding FK
Database Backup Before Migration¶
Development¶
Production¶
# Via docker-compose
docker compose exec v2-postgres pg_dump -U changemaker_v2 changemaker_v2 | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz
# Via backup script
./scripts/backup.sh
Restore from Backup¶
# Stop API services
docker compose stop api media-api
# Restore database
docker compose exec -T v2-postgres psql -U changemaker_v2 changemaker_v2 < backup.sql
# Restart services
docker compose up -d api media-api
CI/CD Integration¶
GitHub Actions Example¶
name: Deploy V2
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- name: Install dependencies
run: cd api && npm ci
- name: Run Prisma migrations
run: cd api && npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Run Drizzle push
run: cd api && npx drizzle-kit push
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Related Documentation¶
- Database Overview — Architecture and models
- Schema Reference — All model fields
- Seeding — Default data
- Prisma Documentation
- Drizzle Documentation