Consent logs that could stand in court
Part 9 of an ongoing series on Moty, a fitness data platform I build and run solo in production: one multi-tenant API, two Next.js frontends. All numbers come from production measurements and git history.
The harder design conversation
When I wrote about encrypting PII with zero downtime, I ended by saying the consent log was the harder design conversation. This is that conversation.
Encryption has a clear adversary, whoever gets the data. Consent has a stranger one: a future dispute about the past. A consent system isn't really answering "did the user click the box." It has to answer whether you can demonstrate, years later, exactly what this person agreed to, when, and under which version of which document. Consent is an evidence problem that ships like a feature, and that framing changed almost every design decision that followed.
Decision one: the log is immutable
Consent records are written once and never updated or deleted while the account lives. Withdrawing consent doesn't edit the old row; it writes a new one. The history is the value. A mutable consent record is testimony you're allowed to rewrite, which is to say, not testimony.
Decision two: refusals are recorded too
The counterintuitive one. When a user declines an optional consent, we write that down with the same ceremony as an agreement.
Why store a "no"? Because absence of a record proves nothing. If the log only contains agreements, then "we never had consent for X" and "we lost the record for X" look identical. Recording the refusal turns a negative you can't prove into a positive you can: asked on this date, declined, under this version. The refusal row is also what makes consent freely given demonstrable: you can show users really did say no and the product kept working.
Decision three: no synthetic consent, ever
Exactly one thing creates a consent row: a real person pressing a button. The system never generates one, not on migration, not on a version bump, not "on the user's behalf."
This rule earns its keep whenever the terms get a minor revision. The tempting shortcut is to auto-carry consents forward by inserting fresh rows that point at the new version. We refused, because those rows would be records of an event that never happened. Minor-revision continuity lives in query logic instead: "consent to version N covers minor revision N.1" is a rule you can read in code, not a fabricated fact sitting in evidence.
Decision four: pin the version, set a clear re-consent bar
Every consent row points at an immutable published version of the exact document agreed to; one id reproduces the full text as the user saw it. Versions carry a major/minor distinction with real consequences: a user is asked to re-consent only if a major version took effect since their last agreement. Minor edits don't interrupt anyone.
That boundary took iteration to get right. An early notification feature for minor revisions was built and then deliberately rolled back when we tightened the interpretation. I keep that revert in the story because policy-shaped features deserve the same willingness to delete as technical ones.
Two more details live at this layer. Documents publish in Korean and English together, and when a user reads a translation, the UI states which language text is legally governing, because a translation dispute is a version dispute in disguise. And consent to health-data processing is its own explicit, separate item, never bundled into general terms.
The lifecycle question I got wrong first
How long do you keep consent evidence? For a while, our internal docs said "statutory retention, typically five years," which, when I finally traced it, had no basis in any of our actual policy documents. We landed on a simpler answer: consent evidence lives exactly as long as the account. The log (including the IP and user-agent captured as evidence context) is retained while the account exists, and destroyed in the same transaction as the withdrawal. There is no grace window where the account is gone but its paperwork isn't.
I like this answer because it's checkable: one lifecycle, stated in the privacy policy, enforced by one code path, with nothing to reconcile. A retention rule you can't trace to a document is a guess, and a guess about evidence protects nothing.
What I'd pass on
Build the consent system as if the reader were a skeptical third party rather than your own product. That one reframe produces every rule above: the immutable log, the recorded refusals, the ban on synthetic rows, the pinned versions, the single traceable lifecycle. None of it is hard to build. All of it is hard to retrofit, because the one thing you cannot backfill is evidence.
0 comments