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>
83 lines
4.2 KiB
Markdown
83 lines
4.2 KiB
Markdown
# 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 collection** — `update: () => 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 bridge** — `currentTraceId()` 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 required** — `AuditEntry.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.
|