Files
agentic-dev-template/docs/decisions/adr-018-audit-and-compliance.md
Danijel Martinek 89d47cce5c docs: strip dead docs/superpowers/ refs across ADRs + guides + glossary
The docs/superpowers/{specs,plans}/ directory was archived to .archive/
in an earlier session (and .archive/ is gitignored). Every md link
into that path is now a broken reference for anyone consuming the
template fresh.

Stripped:
  - ADR-011: **Spec:** header line
  - ADR-015: **Spec:** + **Plan:** header lines
  - ADR-016: **Spec:** + **Plan:** header lines + footer "Spec —"
    bullet (the design rationale is captured in the ADR body itself)
  - ADR-017: **Spec:** + **Plan:** header lines
  - ADR-018: **Spec:** + **Plan:** header lines
  - guides/realtime.md: inline "the full spec" link + footer
    [Spec] entry (folded its description into the ADR-016 entry)
  - guides/events-and-jobs.md: inline "the full spec" link
  - architecture/vertical-feature-spec.md: stale "Deleted" subsection
    referencing docs/superpowers/plans/*

Updated:
  - glossary.md "PRD" entry: clarified status flow now matches the
    shipped pnpm work prd-ship lifecycle (draft -> in-review ->
    approved -> shipped); removed the parenthetical pointing at
    docs/superpowers/specs/ as a definition of "spec"
  - glossary.md "spec" flagged-ambiguity: rewritten to reflect that
    durable design lives in ADRs (docs/decisions/adr-NNN-*.md) and
    implementation seeds live in PRDs (docs/work/prds/*.prd.md) —
    "spec" should be avoided in this template

Preserved (legitimate refs to the SuperPowers plugin, not the dir):
  - agent-first-workflow-and-conformance.md mentions of
    `superpowers:brainstorming` — these reference the external
    plugin skill, not a file in the repo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:00:11 +02:00

4.2 KiB

ADR-018 — Audit Logging & DPA Compliance

Status: Accepted Date: 2026-05-11 Companion guide: docs/guides/audit-and-compliance.md

Context

DPA compliance mandates audit logging for every personal-data access event: VIEW/CREATE/UPDATE/DELETE/EXPORT/PERMISSION_CHANGE, with immutable storage, GDPR-deletable path, centralized aggregation, and strict "what NOT to log" boundaries. The interface decisions from ADR-014 (R31-R51) carry over but audit needs its own channel — observability data is sampled and short-retention, audit data is lossless and long-retention with privileged erasure.

Decision (12 points)

  1. AuditLogProtocol in core-shared — must-have universal surface. Features call ctx.auditLog?.record(entry) without importing the optional package.
  2. AuditEntry type with closed action enum — VIEW/CREATE/UPDATE/DELETE/ EXPORT/PERMISSION_CHANGE; new actions require explicit type bump. No payload/body/oldValue/newValue fields — type enforces "what NOT to log".
  3. @repo/core-audit as 5th optional package — joins realtime, events, trpc, ui. Scaffolded via pnpm turbo gen core-package audit.
  4. Four impls + Recording test double: NoopAuditLog, PayloadAuditLog (local cache), StdoutJsonAuditLog (operator ships via Vector/Fluent Bit), MultiSinkAuditLog (fan-out), RecordingAuditLog (core-testing).
  5. Append-only Payload collectionupdate: () => false access rule is the compliance backbone; erasure path uses overrideAccess: true.
  6. GDPR erasure — sha256-salted pseudonymization (erased-{hash[0:16]}) or hard delete. AUDIT_PSEUDONYM_SALT env REQUIRED in production; bind-time validation fails fast.
  7. Erasure trigger surface — admin tRPC procedure (audit.eraseSubject), Payload afterDelete hook factory (createAuditErasureHook), auth integration via printed generator next-steps (NOT auto-installed).
  8. OTel correlation bridgecurrentTraceId() helper in core-shared; TraceIdEnrichingAuditLog decorator at bind time auto-populates AuditEntry.correlationId from active OTel span. Explicit caller wins.
  9. VIEW capture via BOTH patterns — use-case record() calls (developer decides per-read-path) AND createAuditAfterReadHook factory (opt-in per-collection automatic capture). Fire-and-forget for hooks.
  10. IP/UA explicit at call sites — no AsyncLocalStorage. Callers use truncateIp(raw) (/24 IPv4, /48 IPv6) and pass into record({ from: { ... } }). Sentinels for non-HTTP context: "system" / "background-job".
  11. Multi-tenancy: tenant field requiredAuditEntry.scope.tenant non-optional; single-tenant projects pass "default". Forces multi-tenant thinking from day one.
  12. Six-phase delivery matching established cadence.

Alternatives considered

  • Vendor-coupled SDK (Datadog/Grafana direct) — rejected; couples to vendor.
  • Payload-only sink — fails compliance (hostile-actor immutability).
  • Aggregator-only sink — fails dev ergonomics. Fan-out is the balance.
  • AsyncLocalStorage for request context — rejected per user preference; explicit > implicit.
  • Optional tenant field — rejected; DPA-aligned scope discipline benefits from forcing the question on every call.

Consequences

Positive:

  • DPA-compliant baseline ships with the optional package.
  • Vendor-neutral via stdout JSON + log shipper; any aggregator works.
  • OTel correlation gives compliance auditors one-click pivot to traces.
  • Type-enforced exclusion of "what not to log" prevents categories of mistakes.

Negative:

  • Boilerplate at every record() call site (IP/UA explicit).
  • core-audit ↔ auth coupling for the user-collection hook is awkward (manual install via generator next-steps).
  • StdoutJsonAuditLog's eraseSubject is best-effort (tombstone only; past stdout lines can't be retroactively removed).

Relationship to other ADRs

  • ADR-014 (instrumentation interfaces): audit is a parallel channel, not a signal flowing through OTel. The correlationId field is the bridge.
  • ADR-015 (events/jobs): no overlap; audit is observational, events are reactive.
  • ADR-017 (OTel migration): provides currentTraceId() helper.