Wire PostgreSQL persistence into policy-gateway to persist OPA decisions for compliance/audit requirements per last-mile-plan.md.
Current State: Policy-gateway uses in-memory GovernanceStore (max 1000 entries, lost on restart).
Target State: Persistent storage via PostgreSQL using existing PostgresAuditLogEntryRepository.
What Already Exists:
Add PostgreSQL connection settings with explicit env names:
database_url (env: POLICY_GATEWAY_DATABASE_URL, required; example: postgresql+asyncpg://policy_user:policy_pass@postgres:5432/sea_governance)database_pool_size (env: POLICY_GATEWAY_DATABASE_POOL_SIZE, default: 5)database_max_overflow (env: POLICY_GATEWAY_DATABASE_MAX_OVERFLOW, default: 10)audit_store_backend (env: POLICY_GATEWAY_AUDIT_STORE_BACKEND, default: memory, allowed: memory|postgres)Async database connection manager:
engine via create_async_engine(settings.database_url, pool_size=..., max_overflow=..., pool_pre_ping=True)async_sessionmaker with expire_on_commit=Falseasync def get_session() FastAPI dependency that yields AsyncSession and closes it after requestasync def dispose_engine() for clean shutdown; do not create tables at runtime (migrations handle schema)Update lifespan to manage database lifecycle:
dispose_engine and call it during shutdownUpdate to use PostgreSQL-backed audit persistence:
AsyncSession via Depends(get_session) for audit-related endpointsPostgresAuditLogEntryRepository(session) inside handlersAuditLogEntry entity from evaluation data and call repository.save(...)evaluate_policy where the HTTP response returns before audit persistence completes, causing lost audit entries; ensure the audit save is awaited before returning (or wrap persistence in try/finally so it runs before responding)store.get_audit_log(...) with repository-backed query (see adapter below), keeping API response shape intactrepository.save(...) fails, log the error and return HTTP 500 so compliance failures are visible; do not silently succeedconnect_timeout=5 in POLICY_GATEWAY_DATABASE_URL, rely on SQLAlchemy pool_pre_ping=True, and keep asyncpg default retries; add an application-level timeout (e.g., 3s) around audit writes in evaluate_policyService-local adapter to support filtered audit log queries:
PostgresAuditLogEntryRepository to preserve encapsulation and reuse mapping logicasync def query_filtered(session, start_time, end_time, decision, limit) by composing with repository internals (shared AuditLogEntryTable select); note this is intentionally coupled to the repository mapping until a generated query port existsAuditLogEntryTable.timestamp and AuditLogEntryTable.decisiontimestamp DESC and apply LIMITAuditLogEntry before returning API modelsAdd baseline Alembic files so migrations are runnable:
services/policy-gateway/alembic.iniservices/policy-gateway/migrations/env.pyservices/policy-gateway/migrations/script.py.makoPOLICY_GATEWAY_DATABASE_URLAlembic migration for audit_log_entries table:
id (PK), timestamp (timestamptz), user_id, action, resource, decision, violations (text[]), enforcement_mode, metadata (jsonb)timestamp for time-range queriesdecision for filteringaudit_log_entries table and its timestamp/decision indexes to restore the pre-upgrade schemaRemove database migration logic from the service entrypoint:
alembic upgrade head in-process)RUN_DB_MIGRATIONS handling from the service containerRun migrations as a separate step prior to deploying new replicas:
alembic upgrade headPOLICY_GATEWAY_DATABASE_URL to the migration jobAdd required runtime dependencies:
sqlalchemy[asyncio]>=2.0asyncpg>=0.29alembic>=1.131
2
3
just test-adapters governance-runtime
# Or directly:
pytest libs/governance-runtime/adapters/tests/integration/test_audit_log_entry_repository_integration.py -v
1
pytest services/policy-gateway/tests/test_audit_persistence.py -v
/policy/evaluate writes one row to audit_log_entries/governance/audit returns persisted entriesjust dev-up && just skeleton-upcd services/policy-gateway && python -m uvicorn main:app1
2
3
curl -X POST http://localhost:8080/policy/evaluate \
-H "Content-Type: application/json" \
-d '{"prompt": "test prompt", "user_id": "test-user"}'
1
curl http://localhost:8080/governance/audit?limit=10
http://localhost:5173/governance → Audit Log tab[!WARNING] Breaking Change (Configurable): In-memory behavior is preserved only when
POLICY_GATEWAY_AUDIT_STORE_BACKEND=memoryandPOLICY_GATEWAY_DATABASE_URLis unset. The code path usesservices/policy-gateway/src/store/memory_store.pyand is selected byservices/policy-gateway/src/config/settings.pyinservices/policy-gateway/src/api/routes.py.
Postgres Mode: RequiresPOLICY_GATEWAY_AUDIT_STORE_BACKEND=postgresandPOLICY_GATEWAY_DATABASE_URL=postgresql+asyncpg://policy_user:policy_pass@postgres:5432/sea_governance; migrations must be run via the separate migration step before deploying new replicas.
audit_log_entries)