107 lines
4.1 KiB
Markdown
107 lines
4.1 KiB
Markdown
# Tenant Schema Migrations System
|
|
|
|
**Date:** 2026-04-13
|
|
**Status:** Approved
|
|
|
|
## Problem
|
|
|
|
Horux360 uses a database-per-tenant architecture. When schema changes are made to `createTables()` or `createIndexes()` in `TenantConnectionManager`, only newly provisioned tenants get the updated schema. Existing tenants' databases drift from the expected structure, requiring manual ALTER scripts.
|
|
|
|
## Solution
|
|
|
|
A numbered SQL migration system for tenant databases, with both eager (deploy-time) and lazy (on-connect) execution.
|
|
|
|
## Architecture
|
|
|
|
### Migration Files
|
|
|
|
```
|
|
apps/api/src/migrations/tenant/
|
|
001_initial_schema.sql # Current createTables() + createIndexes()
|
|
002_example_future.sql # Template for future changes
|
|
```
|
|
|
|
- Naming: `NNN_description.sql` (zero-padded 3 digits)
|
|
- Each file must be idempotent (use `IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`, etc.)
|
|
- Files are read from disk at runtime, sorted by version number
|
|
|
|
### Schema Migrations Table (per tenant DB)
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
version INTEGER PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
applied_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
```
|
|
|
|
Created automatically before running any migration.
|
|
|
|
### TenantMigrationRunner
|
|
|
|
New file: `apps/api/src/config/tenant-migrations.ts`
|
|
|
|
**Exported functions:**
|
|
|
|
- `getMigrationFiles()` — Reads and sorts SQL files from migrations directory
|
|
- `getPendingMigrations(pool)` — Compares files vs `schema_migrations` table, returns pending
|
|
- `migrate(pool, databaseName?)` — Applies pending migrations in order, each in its own transaction. Returns count of applied migrations.
|
|
- `migrateAll()` — Queries all active tenants from central DB, calls `migrate()` on each. Logs progress and errors per tenant. Does not stop on individual tenant failure.
|
|
|
|
### Integration Points
|
|
|
|
1. **`TenantConnectionManager.provisionDatabase()`** — Replace `createTables()` + `createIndexes()` calls with `migrate(pool)`. This applies all migrations (starting from 001) to new tenants.
|
|
|
|
2. **`TenantConnectionManager.getPool()`** — After creating or retrieving a pool, call `migrate(pool)` if not already verified this session. Uses `migratedPools: Set<string>` to cache which tenants have been checked. Cache clears on process restart.
|
|
|
|
3. **New Turborepo script `db:migrate-tenants`** — Runs `migrateAll()` for eager deployment. Added to `apps/api/package.json` and root `turbo.json`.
|
|
|
|
4. **`createTables()` and `createIndexes()`** — Removed from `TenantConnectionManager`. Their content moves to `001_initial_schema.sql`.
|
|
|
|
### Lazy Migration Cache
|
|
|
|
```typescript
|
|
// In TenantConnectionManager
|
|
private migratedPools: Set<string> = new Set();
|
|
```
|
|
|
|
- `getPool()` checks `migratedPools.has(tenantId)` before running migrations
|
|
- If not in set → run `migrate(pool)` → add to set
|
|
- Set clears on PM2 restart (new process = fresh set)
|
|
- `invalidatePool()` also removes from `migratedPools`
|
|
|
|
### Deploy Flow
|
|
|
|
```bash
|
|
git pull
|
|
pnpm install
|
|
pnpm build
|
|
pnpm db:migrate-tenants # Eager: apply to all tenants
|
|
pm2 restart all # Lazy: safety net on connect
|
|
```
|
|
|
|
### Adding Future Schema Changes
|
|
|
|
1. Create `NNN_description.sql` in `apps/api/src/migrations/tenant/`
|
|
2. Write idempotent SQL
|
|
3. Deploy — eager applies to all, lazy catches stragglers
|
|
|
|
## Scope Exclusions
|
|
|
|
- No rollback support
|
|
- No data migrations (DDL only; data scripts remain separate)
|
|
- No parallel execution (sequential per tenant)
|
|
- No distributed locking (single PM2 fork instance)
|
|
- No changes to Prisma/central DB migrations
|
|
|
|
## Files Changed
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `apps/api/src/config/tenant-migrations.ts` | NEW — TenantMigrationRunner |
|
|
| `apps/api/src/migrations/tenant/001_initial_schema.sql` | NEW — current createTables + createIndexes |
|
|
| `apps/api/src/config/database.ts` | MODIFY — remove createTables/createIndexes, add lazy migration in getPool, call migrate in provisionDatabase |
|
|
| `apps/api/src/scripts/migrate-tenants.ts` | NEW — eager migration CLI script |
|
|
| `apps/api/package.json` | MODIFY — add db:migrate-tenants script |
|
|
| `turbo.json` | MODIFY — add db:migrate-tenants task |
|