Why one trail
KARE records a single technical audit trail (audit_logs) that serves two needs from one source of truth:
- Compliance / RGPD — who did what, when, on which entity.
- Product history — the timeline on an entity’s detail view (e.g. an anomaly: “Status: Pending → Confirmed by Jean”).
Write path
Every audited write goes throughprisma.auditedTx(fn). Inside it, two helpers append entries to a per-transaction buffer (the sink), which is flushed to audit_logs atomically on commit. A rollback records nothing — there is no audit row for a failed mutation.
For events outside a transaction (login, access),
emit() writes a single best-effort row.
The actor is resolved per request (RS/BO/AD auth) and denormalised into every row, so the trail survives a user’s deletion or anonymisation.
Read path
GET /audit/v1/:entityType/:entityId returns an entity’s timeline: org-scoped, offset-paginated, newest-first. It’s backed by the audit_logs_entity_timeline_idx (entityType, entityId, createdAt desc) index, so the scan is index-ordered (no sort step). The response is the standard { data, totalCount, pagination } envelope; each item carries the actor, the action key (for i18n on the front) and the field diff.
Data model — audit_logs
action is intentionally a free-form string, never a Postgres enum: the catalogue (@lib/audit/action-keys) evolves without a migration.
Actor — not necessarily a User
The actor is a discriminated union, so every action is attributed even when no account is involved:
displayActorName is always set — the human-readable label the timeline shows, even for a system or anonymous action. actorEmail is not exposed on the product timeline (displayActorName is enough).
RGPD
- Redaction at write time — values under secret-looking keys are replaced before storage (
redactValue). - Erasure —
eraseUserFromAudit(userId)anonymises the actor side (anonymizeAuditActor) and clears subject-side PII (eraseAuditSubject: thebefore/after/payloadof rows about that user), keeping who/when/action. - Minimal PII — prefer storing ids + a denormalised
displayActorNameover raw PII, so erasure stays a single update.
Follow-ups (not yet wired)
- Branch
eraseUserFromAuditinto the user-deletion flow. - Retention / purge policy (legal decision on duration) + DB-enforced immutability (
REVOKE UPDATE/DELETE). - PII embedded in other entities’ diffs (e.g. an assignee name) is out of scope of the targeted erasure.