# 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 |
