An auditor asked us a question we could not answer: show me what this user agreed to.
We had the record. User id, timestamp, analytics true, marketing false, policy version 3.2. It looked complete. Then came the follow-up — what did version 3.2 say on that date? Nobody knew. The notice text lived in a CMS that had been edited in place. The version label had not changed, but two sentences about a processor had. The label was a name someone typed, not a fact about the document.
So we had a consent log that proved a user clicked something. It could not prove what.
What that cost was not a fine. It was weeks — reconstructing page history, comparing exports, ending up with a written statement that said we believe. On a compliance question, believe is an expensive word.
We rebuilt it around one rule: a decision is only meaningful next to the thing it answered. Now the notice text, the purpose list and the processor list get hashed at render time, and that hash is stored in the decision record. Not a version label. The content itself. If a sentence changes, the hash changes, and every later decision is visibly bound to different text. Old records stay valid for the old text, which is the honest outcome — consent does not silently migrate to terms the user never saw.
The second thing we found was uglier. Our analytics snippet loaded before the banner did. For a fraction of a second on every first visit the pixel was already firing, and we were asking for permission after taking it. The record said the user decided. The network tab said the decision arrived late. Auditors read network tabs.
So the gate moved. Nothing tracking-related initialises at all until a decision exists for the current notice hash — no decision, no load, not queued and flushed later. Absent is treated as no, not as not yet.
The last piece is the part I would not skip again. Each decision record includes the hash of the previous one, so the log is a chain. If a row is edited or removed afterwards, verification breaks at that point. It does not make us more honest. It makes our honesty checkable by someone who does not trust us, which is the only kind that counts in an audit.
We ended up extracting this into an open-source project we call consentcore, mostly so we would stop rebuilding it per client.
The habit it left me with is broader than cookies. Any record of a decision — terms acceptance, a risk disclosure, a signed quote — is weak evidence unless the exact wording is bound to it cryptographically.
For those of you who have been through a data protection audit: what did they ask for that your logs turned out not to contain?