Initial commit - Horux Despachos NL
This commit is contained in:
106
docs/superpowers/specs/2026-04-13-tenant-migrations-design.md
Normal file
106
docs/superpowers/specs/2026-04-13-tenant-migrations-design.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user