feat(portal-bff): audit log foundation per ADR-0013 #76
Reference in New Issue
Block a user
Delete Branch "feat/portal-bff/audit-module"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Lays down the append-only audit log per ADR-0013: schema declaration, first migration with role grants, NestJS
AuditWriterservice. Typed event-family methods, the separateAUDIT_DATABASE_URLpool, the retention job, and the live-DB integration tests are explicitly listed as "wired as features land" in the ADR's confirmation block — they ship when the matching feature ADRs do.What lands
Prisma schema (
apps/portal-bff/prisma/schema.prisma):multiSchemapreview enabled; datasource declarespublic+auditschemas.AuditEventmodel:id(uuid),createdAt,eventType(free-form in v1),audienceenum (workforce | customer),actorIdHash,traceId,subject,outcomeenum (success | failure | denied),payload(jsonb).createdAt,eventType,traceId— covering the three obvious query shapes.Migration (
prisma/migrations/*_init_audit_schema/migration.sql):CREATE TABLE/ enums output, then the append-only contract re-applied explicitly:ALTER TABLE/TYPE OWNER TO audit_owner.GRANT INSERTtoaudit_writer,SELECTtoaudit_reader,SELECT, DELETEtoaudit_archiver(SELECT is needed to evaluate thecreated_atpredicate of "delete older than retention" — Postgres requires SELECT on every column referenced in DELETE's WHERE).GRANT USAGEon the enum types to all three roles (without itaudit_writer.INSERTfails with "permission denied for type").UPDATE/TRUNCATEto anyone — includingaudit_ownerat runtime; only fresh schema migrations amend the table.Service (
apps/portal-bff/src/audit/):AuditWriter.recordEvent(input)— single entry point. Wraps every INSERT in a transaction whose first statement isSET LOCAL ROLE audit_writer, so the role contract holds at runtime even from the otherwise-privileged BFF connection.traceIdauto-resolved from the active OTel span (so audit row joins with traces and Pino logs on the sametrace_id).actorIdHashauto-resolved from CLS (keyactorIdHash) with explicit input-side override;nullwhen neither is set (placeholder until ADR-0009 / ADR-0010 guards populate CLS).Tests — 8 unit tests on
AuditWriter(mocked Prisma + CLS): role-locking ordering, input pass-through,Prisma.JsonNullfor missing payload, CLS-vs-input precedence onactorIdHash, OTel trace capture, error propagation.End-to-end verification (manual, against local-dev Postgres)
ADR-0013 §Confirmation rewritten
Two-block split: "wired in foundation PR" lists what landed here; "wired as features land" lists the typed event-family methods, AUDIT_DATABASE_URL connection split, startup self-test probe, retention purge job, salt-shared cross-correlation test, and live-DB role-contract integration tests — each anchored to the feature ADR that triggers it.
Recovery for anyone with a pre-existing local-dev DB
If your local-dev Postgres already had the audit migration applied before the SELECT-grant fix, the archiver's DELETE will fail. Two options:
Fresh DBs land with the corrected migration directly.
Out of scope (separate PRs)
signIn,signInFailed, …) — added per matching feature ADR.AUDIT_DATABASE_URLseparate connection pool — defense-in-depth, when production needs it.audit_archiverdaily cron) — phase-3b infra.Test plan
prisma migrate deploysucceeds on a fresh DB (the recovery instructions cover the SELECT-grant gap for already-migrated dev DBs).psql -c "\dp audit.events"shows the expected privilege matrix:audit_owner=arwdDxtm/audit_owner,audit_writer=a/audit_owner,audit_reader=r/audit_owner,audit_archiver=rd/audit_owner.AuditWriter.recordEventfrom a controller (manual smoke once a real flow lands) writes toaudit.eventswith the expectedtrace_idmatching the request's Jaeger span.