Skip to main content

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”).
The same rows feed both: the product timeline is a filtered, projected read of the trail.

Write path

Every audited write goes through prisma.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).
  • ErasureeraseUserFromAudit(userId) anonymises the actor side (anonymizeAuditActor) and clears subject-side PII (eraseAuditSubject: the before/after/payload of rows about that user), keeping who/when/action.
  • Minimal PII — prefer storing ids + a denormalised displayActorName over raw PII, so erasure stays a single update.

Follow-ups (not yet wired)

  • Branch eraseUserFromAudit into 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.