Files
neo/docs/TENANT_MIGRATION_GUIDE.md
Francisco Gaona 516e132611 WIP - move docs
2025-12-24 21:46:05 +01:00

7.9 KiB

Tenant Migration Guide

Quick Start

Create a New Migration

cd backend
npm run migrate:make add_your_feature_name

Edit the generated file in backend/migrations/tenant/

Test on Single Tenant

npm run migrate:tenant acme-corp

Apply to All Tenants

npm run migrate:all-tenants

Available Commands

Command Description
npm run migrate:make <name> Create a new migration file
npm run migrate:tenant <slug> Run migrations for a specific tenant
npm run migrate:all-tenants Run migrations for all active tenants
npm run migrate:latest Run migrations (default DB - rarely used)
npm run migrate:rollback Rollback last migration (default DB)

Architecture

Multi-Tenant Database Structure

┌─────────────────────────┐
│   Central Database      │
│                         │
│  - tenants table        │
│  - users table          │
│  - (encrypted creds)    │
└─────────────────────────┘
            │
            │ manages
            │
    ┌───────┴────────┐
    │                │
┌───▼────┐      ┌───▼────┐
│ Tenant │      │ Tenant │
│   DB1  │  ... │   DBN  │
│        │      │        │
│ - users│      │ - users│
│ - roles│      │ - roles│
│ - apps │      │ - apps │
│ - ...  │      │ - ...  │
└────────┘      └────────┘

How Migrations Work

  1. New Tenant Provisioning (Automatic)

    • User creates tenant via API
    • TenantProvisioningService.provisionTenant() is called
    • Database is created
    • All migrations in migrations/tenant/ are automatically run
    • Tenant status set to ACTIVE
  2. Existing Tenants (Manual)

    • Developer creates new migration file
    • Tests on single tenant: npm run migrate:tenant test-tenant
    • Applies to all: npm run migrate:all-tenants
    • Each tenant database is updated independently

Migration Scripts

migrate-tenant.ts

  • Accepts tenant slug or ID as argument
  • Fetches tenant from central database
  • Decrypts database password
  • Creates Knex connection to tenant DB
  • Runs pending migrations
  • Reports success/failure

migrate-all-tenants.ts

  • Fetches all ACTIVE tenants from central DB
  • Iterates through each tenant
  • Runs migrations sequentially
  • Collects success/failure results
  • Provides comprehensive summary
  • Exits with error if any tenant fails

Security

Password Encryption

Tenant database passwords are encrypted using AES-256-CBC and stored in the central database.

Required Environment Variable:

DB_ENCRYPTION_KEY=your-32-character-secret-key!!

This key must:

  • Be exactly 32 characters (256 bits)
  • Match the key used by backend services
  • Be kept secure (never commit to git)
  • Be the same across all environments accessing tenant DBs

Encryption Flow

Tenant Creation:
  Plain Password → Encrypt → Store in Central DB

Migration Time:
  Encrypted Password → Decrypt → Connect to Tenant DB → Run Migrations

Example Workflow

Adding a New Field to All Tenants

# 1. Create migration
cd backend
npm run migrate:make add_priority_to_tasks

# 2. Edit the migration file
# migrations/tenant/20250127120000_add_priority_to_tasks.js

# 3. Test on staging tenant
npm run migrate:tenant staging-company

# 4. Verify it worked
# Connect to staging DB and check schema

# 5. Apply to all tenants
npm run migrate:all-tenants

Expected output:

🚀 Starting migration for all tenants...

📋 Found 5 active tenant(s)

🔄 Migrating tenant: Acme Corp (acme_corp_db)
✅ Acme Corp: Ran 1 migrations:
   - 20250127120000_add_priority_to_tasks.js

🔄 Migrating tenant: TechStart (techstart_db)
✅ TechStart: Ran 1 migrations:
   - 20250127120000_add_priority_to_tasks.js

...

============================================================
📊 Migration Summary
============================================================
✅ Successful: 5
❌ Failed: 0

🎉 All tenant migrations completed successfully!

Troubleshooting

Error: "Cannot find module '../prisma/generated-central/client'"

Solution: Generate Prisma client

cd backend
npx prisma generate --schema=prisma/schema-central.prisma

Error: "Invalid encrypted password format"

Solution: Check DB_ENCRYPTION_KEY environment variable matches the one used for encryption.

Error: "Migration failed: Table already exists"

Cause: Migration was partially applied or run manually

Solution:

# Check migration status in tenant DB
mysql -h <host> -u <user> -p<pass> <dbname> -e "SELECT * FROM knex_migrations"

# If migration is listed, it's already applied
# If not, investigate why table exists and fix manually

Migration Hangs

Possible causes:

  • Network connection to database lost
  • Database server down
  • Migration has long-running query

Solution: Add timeout to migration and check database connectivity

Best Practices

  1. Test first: Always test migrations on a single tenant before applying to all

  2. Rollback ready: Write down() functions for every migration

  3. Idempotent: Use IF NOT EXISTS clauses where possible

  4. Backup: Take database backups before major migrations

  5. Monitor: Watch the output of migrate:all-tenants carefully

  6. Version control: Commit migration files to git

  7. Document: Add comments explaining complex migrations

  8. Don't skip testing: Never run untested migrations on production

  9. Don't modify: Never modify existing migration files after they're deployed

  10. Don't forget down(): Always implement rollback logic

Integration with TenantProvisioningService

The migrations are also used during tenant provisioning:

// src/tenant/tenant-provisioning.service.ts

async provisionTenant(tenantId: string): Promise<void> {
  // ... create database ...
  
  // Run migrations automatically
  await this.runTenantMigrations(tenant);
  
  // ... update tenant status ...
}

async runTenantMigrations(tenant: any): Promise<void> {
  const knexConfig = {
    client: 'mysql2',
    connection: {
      host: tenant.dbHost,
      port: tenant.dbPort,
      user: tenant.dbUser,
      password: decryptedPassword,
      database: tenant.dbName,
    },
    migrations: {
      directory: './migrations/tenant',
    },
  };

  const knexInstance = knex(knexConfig);
  await knexInstance.migrate.latest();
  await knexInstance.destroy();
}

This ensures every new tenant starts with the complete schema.

CI/CD Integration

Docker Compose

services:
  backend:
    image: your-backend:latest
    command: sh -c "npm run migrate:all-tenants && npm run start:prod"
    environment:
      - DB_ENCRYPTION_KEY=${DB_ENCRYPTION_KEY}

Kubernetes Job

apiVersion: batch/v1
kind: Job
metadata:
  name: tenant-migrations
spec:
  template:
    spec:
      containers:
      - name: migrate
        image: your-backend:latest
        command: ["npm", "run", "migrate:all-tenants"]
        env:
        - name: DB_ENCRYPTION_KEY
          valueFrom:
            secretKeyRef:
              name: db-secrets
              key: encryption-key
      restartPolicy: OnFailure

Further Documentation

Support

For questions or issues:

  1. Check the Backend Scripts README
  2. Review existing migration files in backend/migrations/tenant/
  3. Check Knex documentation: https://knexjs.org/guide/migrations.html