ADR-0005 — Tamper-chain audit log
- Status: Accepted
- Date: 2026-04-18
- Decision-makers: Tom Anderson
Context
Helm holds financial records, customer data, and inventory state. Forensic reconstruction (who did what when) is a hard requirement — both for compliance and for trust. Three approaches considered:
- Plain audit table —
INSERT INTO audit (actor, action, before, after, at) ...on every mutation. Simple. Vulnerable to any DBA-style direct edit of the audit table itself. - Append-only event sourcing — the audit table is the source of truth; primary tables are projections. Heavyweight; reshapes the whole architecture; not aligned with boring tech.
- Hash-chained audit log — like plain audit but with each row's hash incorporating the previous row's hash. Tampering with any past row breaks the chain forward; daily verification detects breaks. Lightweight; doesn't require event-sourcing the rest of the schema.
The chain-hash pattern is well-known (blockchain-style ledgers, git's commit graph, certificate-transparency logs). It's not novel; it is somewhat unusual for a SaaS POS.
Decision
Implement audit as two tables:
audit_events(id, prev_chain_hash, chain_hash, staff_id, action, entity_type, entity_id, at, ip, ua, request_id)audit_mutations(id, event_id, table_name, row_id, before_json, after_json, summary)
chain_hash = sha256(prev_chain_hash || canonical_json(event_row_minus_chain_hash)). The daily cron verifies the chain end-to-end and alerts on any break.
External anchoring (post the latest hash to an external timestamping service daily) is on the roadmap but not in v0.1 scope.
Consequences
Positive:
- Tampering with past rows is detectable, not silent
- Recovery from a tampering event has a known process (the cron alert)
- The pattern is simple to implement and audit (~50 lines of JS)
- Aligns with audit-everything principle
Negative:
- Audit writes are on the critical path of every mutation; can't decouple
- Chain verification is O(N) — for a year of mutations (~1.5M rows), the daily check is a few seconds, acceptable
- The chain is per-database; cross-shop forensics requires per-shop verification
- A tampering event is detectable but not undoable from the audit alone — you'd restore from backups
Mitigations:
- Audit writes are tiny (one INSERT to each of two tables, ~5ms additional latency) — negligible on the request budget
- Monthly cron archives audit older than 90 days to R2; D1 keeps only the live working set
- Backup procedures ensure point-in-time recovery for the rare tampering case
Notes
The chain-hash detection only works if the verification job runs reliably. The daily cron will be monitored (observability). If the cron fails to run for two consecutive days, that's a high-priority alert.
The schema is migrated and the per-endpoint wiring (recordMutation) is live across all 19+ mutation endpoints. The remaining piece is the daily chain-hash verification cron — see current state.