Database Migrations
Lelu manages its own schema through idempotent SQL statements that run at Platform startup. This page explains the migration strategy, how to apply manual changes, and how to roll back safely.
Lelu does not use a migration framework like Flyway or golang-migrate. Migrations are idempotent ALTER TABLE ... IF NOT EXISTS statements embedded in the Platform source.
How Auto-Migration Works
On every startup, the Platform runs a sequence of idempotent SQL statements defined in internal/db/db.go. Each statement is safe to run multiple times.
// Run at startup — all statements are idempotent
migrations := []string{
`CREATE TABLE IF NOT EXISTS policies (...)`,
`ALTER TABLE policies ADD COLUMN IF NOT EXISTS tenant_id TEXT DEFAULT ''`,
`CREATE INDEX IF NOT EXISTS idx_audit_trace ON audit_trails(trace_id)`,
}Adding a New Column
- 1
Edit db.go
SQLALTER TABLE your_table ADD COLUMN IF NOT EXISTS new_column TEXT DEFAULT '';
- 2
Restart the Platform
terminaldocker compose restart platform
- 3
Verify
terminaldocker compose exec postgres psql -U lelu -c "\d your_table"
Zero-Downtime Migrations
To avoid locking tables in production, follow these rules:
Add columns as nullable or with a DEFAULT
PostgreSQL can add NOT NULL columns with DEFAULT values without rewriting the table in PG 11+.
Never rename or drop columns in a single deploy
Rename: add new column → backfill → update code → drop old column across 3 deploys.
Create indexes CONCURRENTLY
Use CREATE INDEX CONCURRENTLY to avoid locking reads/writes on large tables.
Test on a staging replica first
Restore from a production backup and run the migration on a copy before applying to production.
Manual Rollback
Since Lelu auto-migrates forward only, rollbacks must be done manually. Always back up before migrating.
# Take a pre-migration backup pg_dump $DATABASE_URL > backup_$(date +%Y%m%d_%H%M%S).sql # If migration fails, restore psql $DATABASE_URL < backup_20250115_120000.sql