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_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

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:

  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:

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 DROP commands

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_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

# 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 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
}
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:

  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:

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:

  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

# 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

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