Migrations

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.

internal/db/db.go (excerpt)
// 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. 1

    Edit db.go

    SQL
    ALTER TABLE your_table
      ADD COLUMN IF NOT EXISTS new_column TEXT DEFAULT '';
  2. 2

    Restart the Platform

    terminal
    docker compose restart platform
  3. 3

    Verify

    terminal
    docker 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.

Restore from backup
# 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