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
-
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
-
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
-
✅ Test first: Always test migrations on a single tenant before applying to all
-
✅ Rollback ready: Write
down()functions for every migration -
✅ Idempotent: Use
IF NOT EXISTSclauses where possible -
✅ Backup: Take database backups before major migrations
-
✅ Monitor: Watch the output of
migrate:all-tenantscarefully -
✅ Version control: Commit migration files to git
-
✅ Document: Add comments explaining complex migrations
-
❌ Don't skip testing: Never run untested migrations on production
-
❌ Don't modify: Never modify existing migration files after they're deployed
-
❌ 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
- Backend Scripts README - Detailed script documentation
- Multi-Tenant Implementation - Architecture overview
- Multi-Tenant Migration - Migration strategy
Support
For questions or issues:
- Check the Backend Scripts README
- Review existing migration files in
backend/migrations/tenant/ - Check Knex documentation: https://knexjs.org/guide/migrations.html