Migrations
AEGIS uses Alembic for database schema migrations, managed per-service.
Approach
Each service that owns database tables manages its own Alembic migration history. The base schema is defined in SQL files mounted into the PostgreSQL container and executed at startup.
The SQL init files create the foundational schema including Apache AGE setup, pgvector extension, and core tables. Alembic migrations handle incremental changes after initial setup.
Initial Schema
The Docker PostgreSQL container automatically runs the following SQL files on first boot:
| File | Purpose |
|---|---|
00-create-extension-age.sql | Create the AGE extension |
infrastructure/docker/postgres/init.sql | Core tables: audit logs, users, episodic memories, agents, skills, approvals, conversations, budget |
infrastructure/docker/postgres/002_checklist_compliance_tables.sql | Checklist templates, filing checklists, compliance status, rule versions, filing rule snapshots |
infrastructure/docker/postgres/007_entity_type_definitions.sql | Entity type system: type definitions, field definitions, relationship rules, RRC mappings |
These scripts collectively:
- Create the
aegisdatabase and user - Enable extensions:
age,pgvector,uuid-ossp - Create the dev tenant’s per-tenant Apache AGE graph (
tenant_<hex>_oilgas; graphs are per-tenant, not a single globaloilgas) - Create all base tables with indexes and constraints
- Set up audit trail triggers (prevent UPDATE/DELETE on
audit_logs)
Standalone Deploy Migrations
For databases that already exist (and therefore won’t re-run init.sql), standalone idempotent SQL migrations live under infrastructure/deploy/migrations/ and are applied directly with psql:
| File | Purpose |
|---|---|
infrastructure/deploy/migrations/001_users.sql | Create the users table (auth accounts) with the idx_users_email_lower case-insensitive unique index. Idempotent — safe to re-run. |
psql "$DATABASE_URL" -f infrastructure/deploy/migrations/001_users.sqlFresh databases get the users table from init.sql, so this migration is only needed to bring existing databases up to date. Since R42a the auth-service migration runner (below) re-ships 001/002 idempotently, so the manual psql step is no longer required for auth tables.
Core Numbered Migrations (infrastructure/docker/postgres/)
Numbered migrations 002–031 live alongside init.sql. They do not auto-apply: a fresh Docker volume gets only the files docker-compose mounts into docker-entrypoint-initdb.d (init.sql, 002, 007, 015, and agent-config’s 001) — everything else, including 030/031, must be applied manually with psql, in numeric order (the box deploy flow applies new files by hand before restarting; local volumes need the same). The two newest:
| File | Purpose |
|---|---|
030_event_number_sequences_reconcile.sql | Event-numbering split: event_number_sequences = platform events keyed by event_type_enum; custom_event_number_sequences = custom events keyed by an event_type_id UUID FK → event_type_definitions. Migration 010’s rename of the custom table is tombstoned (it collided with 004’s table on every ordered application); 030 converges every historical shape and no-ops where flaring’s schema is absent. |
031_identity_normalization.sql | R42e identity normalization: seeds the four non-loginable system users (system@aegis.local = nil UUID, legacy@, eval-runner@ — only if the email is absent, demo-seed@), creates identity_map(legacy_id TEXT PK, user_id UUID FK users, source TEXT), retypes VARCHAR/TEXT identity columns to uuid via convergent UPDATE + guarded ALTER TYPE, and adds FKs → users(id) ON DELETE RESTRICT (NOT VALID then VALIDATE) on ~17 core columns. audit_logs.actor_id stays TEXT permanently — append-only history resolves at read time through identity_map. Safe to re-apply. See the schema page. |
Self-Applying Service Migrations (auth-service, R42a)
auth-service owns its numbered SQL migrations under
services/auth-service/src/auth_service/migrations/ and self-applies them on every
boot (same convention as agent-config-service: every statement catalog-guarded, no
applied-tracking). The runner is auth_service/migrate.py, also runnable standalone as
poetry run python -m auth_service.migrate. After the SQL it converges the RBAC seeds
(4 system roles + the Appendix C permission matrix), a one-time backfill of
user_roles from the legacy users.roles array, and (R42e) the system-user +
identity_map seeds — then drops the users.roles shadow column once the
backfill has run, so grants live solely in user_roles.
| File | Purpose |
|---|---|
001_users.sql / 002_user_tenant.sql | Idempotent re-ships of the deploy migrations so a fresh DB converges on boot |
003_rbac_substrate.sql | R42a — roles, role_permissions, user_roles, user_invites; users.token_version + users.password_set_at (with a convergent password_set_at backfill for existing users) |
The CI lane auth-service-tests runs ci/auth_double_boot.sh: boot → snapshot →
boot again → snapshot; both boots must succeed (health + bootstrap-admin login) with
byte-identical schema and seed state. A non-idempotent migration fails this lane
instead of boot-looping the box.
Self-Applying Service Migrations (agent-config-service)
The agent-config-service owns its numbered SQL migrations under
services/agent-config-service/src/agent_config/migrations/ and self-applies them
on every boot (each statement is idempotent — there is no applied-tracking table, so
files must stay safe to re-run against newer state). Notable recent addition:
| File | Purpose |
|---|---|
016_platform_settings.sql | Platform Settings: creates platform_settings (tenant LLM runtime config) and user_budget_overrides (per-user daily caps); widens prompt_audit_log.action to VARCHAR(64) and re-creates its CHECK with the three platform_settings.* audit actions (016 is now the constraint’s sole owner). |
017_llm_call_settings.sql | Adds the per-LLM-call knobs to platform_settings: max_output_tokens_per_call (per-call max_tokens ceiling — thinking + visible output) and thinking_mode (adaptive | disabled, sent explicitly to Anthropic models). Fixes Claude Sonnet 5’s adaptive-thinking default consuming a small ceiling and returning empty messages. |
018_tenant_branding.sql | E0.3 — adds tenant_branding TEXT to platform_settings: the free-text label the sidebar footer shows (replaces the hardcoded “RRC District 08 / Permian Basin”). NULL = no footer line; no env fallback tier. |
020_identity_normalization.sql | R42e — identity normalization for the agent-config-owned tables: retypes prompt_versions.author_id/draft_owner_id/approved_by, namespace_access_control.user_id, platform_settings.updated_by, and user_budget_overrides identity columns to uuid, and remaps skill ownership from the retired pre-R42 admin-author sentinel UUID to the real admin. prompt_audit_log.actor_id is deliberately untouched (append-only, HMAC-verified — resolves at read time via identity_map). Skip-and-converge: each step no-ops until users/identity_map exist. |
021_prompt_target_roles.sql | R42e — adds prompt_versions.target_roles TEXT[] for role-based pre-production targeting (served on target_users match OR role overlap). |
See the Database Schema page for column details.
Self-Applying Service Migrations (connector-service, R44a)
connector-service owns its numbered SQL migrations under
services/connector-service/src/connector_service/migrations/ and self-applies them
on every boot (auth-service runner pattern: every statement catalog-predicate-guarded,
no applied-tracking, each file in its own transaction). The runner is
connector_service/migrate.py, also runnable standalone as
poetry run python -m connector_service.migrate. A failing migration boot-loops the
service by design.
| File | Purpose |
|---|---|
001_connector_catalog.sql | R44a — integration_connectors (the external data source catalog; credential_ref only, never secret material), connector_schema_snapshots (discovery snapshots, is_current flag), connector_execution_log (governance trail for R44b/R44c, params_hash only) |
The CI double-boot test ci/connector_double_boot.sh guards the idempotency
invariant (boot → boot again → schema must be identical).
Running Migrations
For services with Alembic configured:
# Navigate to the service directory
cd services/{service-name}
# Create a new migration
poetry run alembic revision --autogenerate -m "description of change"
# Run pending migrations
poetry run alembic upgrade head
# Check current migration status
poetry run alembic current
# Roll back one migration
poetry run alembic downgrade -1
# View migration history
poetry run alembic historyMigration Workflow
Creating a New Migration
- Make your model or schema changes in the service code.
- Run the autogenerate command to create a migration file:
cd services/{service-name}
poetry run alembic revision --autogenerate -m "add compliance_status table"- Review the generated migration in
alembic/versions/. Autogenerate is not perfect — verify that theupgrade()anddowngrade()functions are correct. - Test the migration in both directions:
# Apply
poetry run alembic upgrade head
# Verify
poetry run alembic current
# Roll back
poetry run alembic downgrade -1
# Re-apply
poetry run alembic upgrade head- Commit the migration file alongside the code changes.
Checking Migration Status
# Show current revision
poetry run alembic current
# Show all pending migrations
poetry run alembic history --indicate-current
# Show the SQL that would be run (without executing)
poetry run alembic upgrade head --sqlConventions
- One migration per logical change — don’t bundle unrelated schema changes into a single revision
- Descriptive revision messages — e.g.,
add compliance_status tablenotupdate schema - Always test migrations — run
upgradeanddowngradebefore committing - Never modify existing migrations — if a migration has been applied by other developers, create a new corrective migration instead
- Schema ownership — only the owning service should create migrations for its tables
- Idempotent where possible — use
IF NOT EXISTSguards in raw SQL within migrations
Adding Alembic to a New Service
If a service needs to manage its own tables, set up Alembic:
cd services/{service-name}
poetry add alembic asyncpg sqlalchemy
poetry run alembic init alembicConfigure alembic.ini with the database URL:
sqlalchemy.url = postgresql://aegis:aegis_local@localhost:5432/aegisUpdate alembic/env.py to import your models for autogenerate support:
from your_service.models import Base
target_metadata = Base.metadataIf using async database connections (which all AEGIS services do), you will need to configure Alembic’s env.py to use an async engine. See the SQLAlchemy docs on async migrations .
Resetting the Database
During development, you may need to start fresh. The most reliable way is to remove the Docker volume and recreate:
# Stop containers and remove volumes
docker compose down -v
# Recreate containers (init.sql runs automatically)
docker compose up -d
# Re-run seed scripts
curl -X POST http://localhost:8003/seed
cd services/orchestration-engine
poetry run python -m orchestration.seed_checklists
poetry run python -m orchestration.seed_rules
poetry run python -m orchestration.seed_demo_dataRunning docker compose down -v destroys all data in PostgreSQL and Redis. Only use this in local development. Never run this against a shared or production database.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
alembic.util.exc.CommandError: Can't locate revision | Migration file deleted or corrupted | Reset Alembic version table: DELETE FROM alembic_version then re-run |
relation already exists | Schema was created by init.sql before Alembic | Mark migration as applied: poetry run alembic stamp head |
target database is not up to date | Pending migrations exist | Run poetry run alembic upgrade head first |
No changes detected during autogenerate | Models not imported in env.py | Verify target_metadata is set correctly |