// Prisma schema for portal-bff. // // `multiSchema` preview is enabled because per ADR-0013 the audit log // lives in its own `audit` schema with role-based append-only access // (audit_owner / audit_writer / audit_reader / audit_archiver). The // public schema holds the regular business data; only audit.events // lives in audit. generator client { provider = "prisma-client-js" previewFeatures = ["multiSchema"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") schemas = ["public", "audit"] } // ============================================================ // Audit log (per ADR-0013) // ============================================================ // // Append-only by Postgres role grants — the schema, roles, and // default privileges are provisioned by infra/local/init/postgres/ // 01-init.sql in dev and the equivalent production manifest. The // migration that creates this table re-applies the grants // explicitly and ALTERs the table owner to `audit_owner` so the // runtime role contract holds even when the migration runs as a // privileged migrator account. // // At runtime, the BFF wraps every INSERT into this table in a // transaction that begins with `SET LOCAL ROLE audit_writer`, so // even a compromised BFF connection cannot UPDATE / TRUNCATE / // DELETE — those grants are not on `audit_writer`. enum AuditAudience { workforce customer @@schema("audit") } enum AuditOutcome { success failure denied @@schema("audit") } model AuditEvent { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) eventType String @map("event_type") audience AuditAudience // Salted hash (LOG_USER_ID_SALT) of the actor's stable id. NULL // when the actor is unauthenticated (e.g. failed login attempt // before resolving an identity). The same salt is used by the // BFF Pino logger so audit and app logs cross-correlate on this // field. actorIdHash String? @map("actor_id_hash") // W3C trace id (32 hex chars) of the request that produced the // event. Cross-correlates with traces in Jaeger and with Pino // log lines that carry the same `trace_id` field. NULL only if // the event was emitted outside any inbound request (e.g. a // future cron job). traceId String? @map("trace_id") // Free-form identifier of what the event is *about* — typically // a domain entity URI like `user:42` or `dossier:xyz`. NULL when // the event is system-wide and has no clear subject. subject String? outcome AuditOutcome // Event-specific structured detail. Redaction of PII is the // caller's responsibility; the BFF Pino redact list is the // reference allow-/deny-list. payload Json? @@map("events") @@schema("audit") @@index([createdAt]) @@index([eventType]) @@index([traceId]) }