Database Migrations - ThriveSend B2B2G
Database Migrations - ThriveSend B2B2G
Section titled “Database Migrations - ThriveSend B2B2G”Last Updated: January 26, 2026 Status: Production Developer Guide: Database Layer
Overview
Section titled “Overview”This guide covers database migrations for ThriveSend B2B2G using Prisma Migrate. Learn how to create, apply, and manage schema changes while maintaining POPIA compliance and data integrity.
Migration Strategy:
- Schema-first approach with Prisma
- Version-controlled SQL migrations
- Zero-downtime production deployments
- POPIA-compliant data transformations
Migration Workflow
Section titled “Migration Workflow”Development Workflow
Section titled “Development Workflow”graph LR A[Edit Schema] --> B[Create Migration] B --> C[Review SQL] C --> D[Apply Migration] D --> E[Test Changes] E --> F[Commit to Git] F --> G[Deploy to Production]Key Commands
Section titled “Key Commands”| Command | Purpose | Environment |
|---|---|---|
npx prisma migrate dev | Create and apply migration | Development |
npx prisma migrate deploy | Apply pending migrations | Production |
npx prisma migrate status | Check migration status | All |
npx prisma migrate resolve | Mark migration as applied | Production (recovery) |
npx prisma db push | Sync schema without migration | Development (prototyping) |
Creating Migrations
Section titled “Creating Migrations”Step 1: Update Prisma Schema
Section titled “Step 1: Update Prisma Schema”Edit prisma/schema.prisma to add or modify models:
// Example: Adding a new field to Client modelmodel Client { id String @id @default(cuid()) organizationId String name String
// NEW FIELD: Contact email (required for POPIA compliance) contactEmail String // Added field
createdAt DateTime @default(now()) updatedAt DateTime @updatedAt}Step 2: Create Migration
Section titled “Step 2: Create Migration”# Create migration with descriptive namenpx prisma migrate dev --name make_contact_email_required
# What happens:# 1. Prisma analyzes schema changes# 2. Generates SQL migration file# 3. Applies migration to development database# 4. Regenerates Prisma ClientMigration Output:
Environment variables loaded from .env.localPrisma schema loaded from prisma/schema.prismaDatasource "db": PostgreSQL database "thrivesend_dev", schema "public" at "localhost:5432"
Applying migration `20251002193302_make_contact_email_required`
The following migration(s) have been created and applied from new schema changes:
migrations/ └─ 20251002193302_make_contact_email_required/ └─ migration.sql
Your database is now in sync with your schema.
✔ Generated Prisma ClientStep 3: Review Generated SQL
Section titled “Step 3: Review Generated SQL”Check the generated migration file:
-- migrations/20251002193302_make_contact_email_required/migration.sql
/* Warnings: - Made the column `contactEmail` on table `clients` required. This step will fail if there are existing NULL values in that column.*/
-- AlterTableALTER TABLE "public"."clients" ALTER COLUMN "contactEmail" SET NOT NULL;Step 4: Test the Migration
Section titled “Step 4: Test the Migration”// Test in developmentimport { prisma } from '@/lib/db'
// This should now work (contactEmail is required)const client = await prisma.client.create({ data: { organizationId: 'org_123', name: 'Test Client', contactEmail: 'contact@example.com', // Now required }})
// This should fail (missing contactEmail)const invalidClient = await prisma.client.create({ data: { organizationId: 'org_123', name: 'Invalid Client', // Missing contactEmail - will throw error }})// Error: Field 'contactEmail' is requiredReal-World Migration Examples
Section titled “Real-World Migration Examples”Example 1: Adding Invitation System (POPIA Compliant)
Section titled “Example 1: Adding Invitation System (POPIA Compliant)”This is a real migration from the ThriveSend B2B2G codebase:
-- migrations/20251002193302_make_contact_email_required/migration.sql
-- Create enums for invitation systemCREATE TYPE "public"."InvitationRole" AS ENUM ( 'OWNER', 'ADMIN', 'MANAGER', 'CONTENT_CREATOR', 'REVIEWER', 'APPROVER', 'PUBLISHER', 'ANALYST', 'VIEWER', 'GOVERNMENT_LIAISON', 'COMPLIANCE_OFFICER');
CREATE TYPE "public"."InvitationStatus" AS ENUM ( 'PENDING', 'SENT', 'OPENED', 'ACCEPTED', 'EXPIRED', 'REVOKED', 'BOUNCED', 'FAILED');
CREATE TYPE "public"."EmailStatus" AS ENUM ( 'PENDING', 'SENT', 'DELIVERED', 'OPENED', 'CLICKED', 'BOUNCED', 'COMPLAINED', 'FAILED', 'PROCESSED');
-- Create invitations tableCREATE TABLE "public"."invitations" ( "id" TEXT NOT NULL, "token" TEXT NOT NULL, "email" TEXT NOT NULL, "organizationId" TEXT NOT NULL, "role" "public"."InvitationRole" NOT NULL, "securityClearance" "public"."SecurityClearanceLevel" NOT NULL, "clientAccess" TEXT[], "invitedBy" TEXT NOT NULL, "status" "public"."InvitationStatus" NOT NULL DEFAULT 'PENDING', "emailSentAt" TIMESTAMP(3), "emailOpenedAt" TIMESTAMP(3), "acceptedAt" TIMESTAMP(3), "expiredAt" TIMESTAMP(3) NOT NULL, "popiaConsentGiven" BOOLEAN NOT NULL DEFAULT false, "consentMetadata" JSONB, "auditMetadata" JSONB NOT NULL DEFAULT '{}', "customMessage" TEXT, "ipAddress" TEXT, "userAgent" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "invitations_pkey" PRIMARY KEY ("id"));
-- Create POPIA consent records tableCREATE TABLE "public"."popia_consent_records" ( "id" TEXT NOT NULL, "invitationId" TEXT, "userId" TEXT, "email" TEXT NOT NULL, "organizationId" TEXT NOT NULL, "consentType" TEXT NOT NULL DEFAULT 'INVITATION_ACCEPTANCE', "dataProcessingConsent" BOOLEAN NOT NULL, "marketingConsent" BOOLEAN NOT NULL DEFAULT false, "analyticsConsent" BOOLEAN NOT NULL DEFAULT false, "consentVersion" TEXT NOT NULL DEFAULT '1.0', "consentLanguage" TEXT NOT NULL DEFAULT 'en', "ipAddress" TEXT NOT NULL, "userAgent" TEXT NOT NULL, "consentTimestamp" TIMESTAMP(3) NOT NULL, "withdrawnAt" TIMESTAMP(3), "withdrawalReason" TEXT, "auditMetadata" JSONB NOT NULL DEFAULT '{}', "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "popia_consent_records_pkey" PRIMARY KEY ("id"));
-- Create indexes for performanceCREATE UNIQUE INDEX "invitations_token_key" ON "public"."invitations"("token");CREATE INDEX "invitations_email_idx" ON "public"."invitations"("email");CREATE INDEX "invitations_organizationId_idx" ON "public"."invitations"("organizationId");CREATE INDEX "popia_consent_records_email_idx" ON "public"."popia_consent_records"("email");
-- Add foreign key constraintsALTER TABLE "public"."invitations" ADD CONSTRAINT "invitations_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "public"."popia_consent_records" ADD CONSTRAINT "popia_consent_records_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;What This Migration Does:
- Creates invitation system with POPIA compliance
- Adds consent tracking for data protection
- Includes audit metadata for all actions
- Enforces SA data residency (dataResidency field)
- Creates performance indexes
Example 2: Adding Political Organization Support
Section titled “Example 2: Adding Political Organization Support”-- migrations/20251002203425_add_political_organization_support/migration.sql
-- Create enum for political levelsCREATE TYPE "public"."PoliticalLevel" AS ENUM ( 'NATIONAL', 'PROVINCIAL', 'LOCAL', 'INDEPENDENT');
-- Add political organization fields to clients tableALTER TABLE "public"."clients" ADD COLUMN "politicalLevel" "public"."PoliticalLevel", ADD COLUMN "iecRegistrationNumber" TEXT, ADD COLUMN "electoralConstituency" TEXT, ADD COLUMN "partyAffiliation" TEXT;
-- Create index for IEC lookupsCREATE INDEX "clients_iecRegistrationNumber_idx" ON "public"."clients"("iecRegistrationNumber");Applying Migrations
Section titled “Applying Migrations”Development Environment
Section titled “Development Environment”# Apply all pending migrationsnpx prisma migrate dev
# Apply migrations and skip promptsnpx prisma migrate dev --skip-generate
# Create migration without applying (dry run)npx prisma migrate dev --create-only --name add_new_fieldProduction Environment
Section titled “Production Environment”# IMPORTANT: Never use `migrate dev` in production!
# Apply pending migrations (production-safe)npx prisma migrate deploy
# Check migration status firstnpx prisma migrate statusProduction Migration Process:
# On production servercd /var/www/thrivesend
# 1. Check current statusnpx prisma migrate status
# 2. Backup databasepg_dump thrivesend_b2b2g > backup_$(date +%Y%m%d_%H%M%S).sql
# 3. Apply migrationsnpx prisma migrate deploy
# 4. Verify successnpx prisma migrate status
# 5. Restart applicationpm2 restart thrivesendMigration Best Practices
Section titled “Migration Best Practices”-
Use descriptive migration names:
Terminal window # ✅ GOODnpx prisma migrate dev --name add_contact_email_to_clientsnpx prisma migrate dev --name add_security_clearance_levels# ❌ BADnpx prisma migrate dev --name updatenpx prisma migrate dev --name fix -
Review SQL before applying:
Terminal window # Create migration without applyingnpx prisma migrate dev --create-only --name my_migration# Review the SQLcat prisma/migrations/*/migration.sql# Apply manually if needednpx prisma migrate dev -
Test migrations on development database first:
Terminal window # Test on dev databaseDATABASE_URL="postgresql://localhost:5432/thrivesend_dev" \npx prisma migrate dev# Verify it works# Then apply to production -
Include data transformations when needed:
-- Good example: Update existing records before adding constraintUPDATE clients SET contactEmail = 'info@example.com'WHERE contactEmail IS NULL;ALTER TABLE clients ALTER COLUMN contactEmail SET NOT NULL; -
Add indexes for POPIA queries:
-- Add indexes for audit log queriesCREATE INDEX "audit_logs_timestamp_idx"ON "public"."audit_logs"("timestamp");CREATE INDEX "audit_logs_userId_idx"ON "public"."audit_logs"("userId");
❌ DON’T
Section titled “❌ DON’T”-
Edit applied migrations:
Terminal window # ❌ NEVER edit migrations that have been applied# Once a migration is in production, it's immutable# ✅ Instead, create a new migrationnpx prisma migrate dev --name fix_previous_migration -
Use
prisma db pushin production:Terminal window # ❌ DANGEROUS - No migration historynpx prisma db push# ✅ Always use migrate deploynpx prisma migrate deploy -
Delete migrations:
Terminal window # ❌ NEVER delete migration filesrm -rf prisma/migrations/# This breaks production deployments -
Skip backing up before production migrations:
Terminal window # ❌ RISKYnpx prisma migrate deploy# ✅ SAFEpg_dump thrivesend_b2b2g > backup.sqlnpx prisma migrate deploy
Checking Migration Status
Section titled “Checking Migration Status”View Migration Status
Section titled “View Migration Status”# Check which migrations are appliednpx prisma migrate statusExample Output:
Status: All migrations have been applied
Database schema is up to date!
Applied migrations: 20250924095149_init 20251002193302_make_contact_email_required 20251002203425_add_political_organization_supportWhen Migrations Are Pending
Section titled “When Migrations Are Pending”Status: 2 migrations have not been applied
Pending migrations: 20260115120000_add_campaign_analytics 20260118140000_add_security_clearance_expiry
To apply pending migrations, run: npx prisma migrate deployHandling Migration Issues
Section titled “Handling Migration Issues”Issue: Migration Failed Midway
Section titled “Issue: Migration Failed Midway”Scenario: Migration partially applied, database in inconsistent state.
# 1. Check statusnpx prisma migrate status# Shows: Migration failed at step X
# 2. Restore from backuppsql thrivesend_dev < backup_20260126.sql
# 3. Fix the migration SQL# Edit: prisma/migrations/TIMESTAMP_name/migration.sql
# 4. Try againnpx prisma migrate devIssue: Schema Drift Detected
Section titled “Issue: Schema Drift Detected”Scenario: Database schema doesn’t match Prisma schema.
Error: The database schema is not in sync with your migration history.
To fix: 1. Run `npx prisma migrate dev` (development) 2. Run `npx prisma migrate deploy` (production)Solution:
# Development: Sync and create migrationnpx prisma migrate dev --name sync_schema
# Production: Review and apply carefullynpx prisma migrate statusnpx prisma migrate deployIssue: Baseline Existing Database
Section titled “Issue: Baseline Existing Database”Scenario: Adding Prisma to existing database with data.
# 1. Create initial migration from schemanpx prisma migrate dev --name init --create-only
# 2. Mark migration as applied (don't run SQL)npx prisma migrate resolve --applied init
# 3. Continue with normal workflownpx prisma migrate dev --name your_next_migrationPOPIA Compliance Considerations
Section titled “POPIA Compliance Considerations”Data Transformations
Section titled “Data Transformations”When migrating data, ensure POPIA compliance:
-- ❌ BAD: Exposes personal information in migrationUPDATE clients SET email = 'test@example.com';
-- ✅ GOOD: Anonymize while preserving structureUPDATE clients SET email = CONCAT('user', id, '@example.com')WHERE env = 'development';
-- Log the transformationINSERT INTO audit_logs (action, resourceType, details, timestamp)VALUES ( 'DATA_MIGRATION', 'Client', '{"migration": "anonymize_dev_data", "count": ' || ROW_COUNT || '}', NOW());Consent Migrations
Section titled “Consent Migrations”When adding consent fields, handle existing users:
-- Add consent fieldsALTER TABLE organization_users ADD COLUMN "popiaConsentGiven" BOOLEAN DEFAULT false, ADD COLUMN "consentDate" TIMESTAMP(3);
-- Create consent records for existing usersINSERT INTO popia_consent_records ( userId, email, organizationId, dataProcessingConsent, consentTimestamp, ipAddress, userAgent)SELECT id, email, organizationId, false, -- Require re-consent NOW(), '0.0.0.0', -- Unknown IP for existing users 'migration'FROM organization_usersWHERE popiaConsentGiven IS NULL;Audit Log Migrations
Section titled “Audit Log Migrations”Always log schema changes:
-- After migration, log the changeINSERT INTO audit_logs ( action, resourceType, details, timestamp)VALUES ( 'SCHEMA_MIGRATION', 'Database', '{"migration": "add_political_organization_support", "models": ["Client"]}', NOW());Zero-Downtime Migrations
Section titled “Zero-Downtime Migrations”For production deployments with minimal downtime:
Strategy 1: Additive Changes
Section titled “Strategy 1: Additive Changes”-- Phase 1: Add new column (nullable)ALTER TABLE clients ADD COLUMN newField TEXT;
-- Deploy code that writes to both old and new fields
-- Phase 2: Backfill dataUPDATE clients SET newField = oldField WHERE newField IS NULL;
-- Phase 3: Make column requiredALTER TABLE clients ALTER COLUMN newField SET NOT NULL;
-- Phase 4: Remove old columnALTER TABLE clients DROP COLUMN oldField;Strategy 2: Expand-Contract Pattern
Section titled “Strategy 2: Expand-Contract Pattern”-- Expand: Add new structureCREATE TABLE clients_new ( -- new schema);
-- Migrate dataINSERT INTO clients_new SELECT * FROM clients;
-- Contract: Replace old tableDROP TABLE clients;ALTER TABLE clients_new RENAME TO clients;Migration Checklist
Section titled “Migration Checklist”Before deploying migrations to production:
- Migration tested on development database
- Migration reviewed by team member
- Database backup created
- Migration SQL reviewed for POPIA compliance
- Rollback plan documented
- Production maintenance window scheduled (if needed)
- Monitoring alerts configured
- Post-migration verification queries prepared
Example Checklist:
## Migration: add_campaign_analytics- [x] Created migration: `npx prisma migrate dev --name add_campaign_analytics`- [x] Tested on local database- [x] Reviewed SQL file- [x] POPIA compliance verified (no PII in migration)- [x] Backup plan: `pg_dump thrivesend_b2b2g > backup_20260126.sql`- [x] Rollback SQL prepared: `DROP TABLE campaign_analytics;`- [x] Deployment window: 2026-01-27 02:00 AM SAST (low traffic)- [x] Verification query: `SELECT COUNT(*) FROM campaign_analytics;`Next Steps
Section titled “Next Steps”- ← Prisma Setup - Database configuration
- Schema Management → - B2B2G schema design
- Querying Data → - Query patterns and best practices
Related Documentation:
- Service Architecture - Using Prisma in services
Last Updated: January 26, 2026