Skip to Content

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:

FilePurpose
00-create-extension-age.sqlCreate the AGE extension
infrastructure/docker/postgres/init.sqlCore tables: audit logs, users, episodic memories, agents, skills, approvals, conversations, budget
infrastructure/docker/postgres/002_checklist_compliance_tables.sqlChecklist templates, filing checklists, compliance status, rule versions, filing rule snapshots
infrastructure/docker/postgres/007_entity_type_definitions.sqlEntity type system: type definitions, field definitions, relationship rules, RRC mappings

These scripts collectively:

  1. Create the aegis database and user
  2. Enable extensions: age, pgvector, uuid-ossp
  3. Create the dev tenant’s per-tenant Apache AGE graph (tenant_<hex>_oilgas; graphs are per-tenant, not a single global oilgas)
  4. Create all base tables with indexes and constraints
  5. 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:

FilePurpose
infrastructure/deploy/migrations/001_users.sqlCreate 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.sql

Fresh 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 002031 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:

FilePurpose
030_event_number_sequences_reconcile.sqlEvent-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.sqlR42e 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.

FilePurpose
001_users.sql / 002_user_tenant.sqlIdempotent re-ships of the deploy migrations so a fresh DB converges on boot
003_rbac_substrate.sqlR42aroles, 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:

FilePurpose
016_platform_settings.sqlPlatform 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.sqlAdds 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.sqlE0.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.sqlR42e — 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.sqlR42e — 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.

FilePurpose
001_connector_catalog.sqlR44aintegration_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 history

Migration Workflow

Creating a New Migration

  1. Make your model or schema changes in the service code.
  2. Run the autogenerate command to create a migration file:
cd services/{service-name} poetry run alembic revision --autogenerate -m "add compliance_status table"
  1. Review the generated migration in alembic/versions/. Autogenerate is not perfect — verify that the upgrade() and downgrade() functions are correct.
  2. 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
  1. 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 --sql

Conventions

  • One migration per logical change — don’t bundle unrelated schema changes into a single revision
  • Descriptive revision messages — e.g., add compliance_status table not update schema
  • Always test migrations — run upgrade and downgrade before 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 EXISTS guards 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 alembic

Configure alembic.ini with the database URL:

sqlalchemy.url = postgresql://aegis:aegis_local@localhost:5432/aegis

Update alembic/env.py to import your models for autogenerate support:

from your_service.models import Base target_metadata = Base.metadata

If 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_data

Running 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

ProblemCauseSolution
alembic.util.exc.CommandError: Can't locate revisionMigration file deleted or corruptedReset Alembic version table: DELETE FROM alembic_version then re-run
relation already existsSchema was created by init.sql before AlembicMark migration as applied: poetry run alembic stamp head
target database is not up to datePending migrations existRun poetry run alembic upgrade head first
No changes detected during autogenerateModels not imported in env.pyVerify target_metadata is set correctly
Last updated on