// teardown
The consent registry
The cookie policy is the code.
Source of truth
Every visit
a registry change asks everyone again
// overview
A cookie banner, a consent platform and a hand-written cookie policy are usually three artifacts with three owners. Here they are one typed registry of every cookie and storage key: the writers enforce it, the banner renders it and the policy page prints it. A build step fails when code writes around it, and every consent receipt records which version of it the visitor agreed to.
// how it works
01 · register
One list, three jobs
Every cookie, localStorage key and sessionStorage key the site writes has one entry in a typed registry: name, category, provider, retention and a plain-English purpose. That array is the writers' allowlist, the category list the banner renders, and the table the cookie policy publishes. Because they are one artifact, the policy a visitor reads and the rules the code enforces cannot disagree. Keys built at runtime live under fixed namespaces, so they resolve to an entry too.
02 · verify
The build refuses drift
A static check runs in CI and inside the production build, before the app compiles. Writing document.cookie anywhere but the cookie module fails it. Touching localStorage or sessionStorage outside the namespaced wrapper fails it, bar a short reviewed allowlist. Every literal name handed to the cookie writer or to a server-side cookie set must resolve to a registry entry. Adding a cookie without describing it is not a review comment. It stops the deploy.
03 · publish
The policy prints the registry
The cookie policy page maps over the same array the runtime enforces, grouped by category, and prints each entry's name, type, provider, retention and purpose verbatim. Retention is more than a description: the cookie writer parses the same expiry string to set the real expiry, so a published 90 days is a 90-day cookie. Editing an entry changes what the code enforces and what the policy discloses in the same commit.
04 · locate
Posture comes from the edge
Whether a visitor must choose before anything beyond the strictly necessary is stored, or sees a dismissible notice, depends on where they are. That answer comes from the hosting edge's geolocation headers, read in one small dynamic route, never from the browser, which could claim to be anywhere. Returning visitors with a current decision skip the call. On a first visit, a browser sending Global Privacy Control starts with analytics and marketing off, recorded as its own basis.
05 · ask
The banner counts from the registry
The banner and preference center offer the registry's three decidable categories: functional, analytics and embedded media. Each row shows how many entries the category holds and who provides them, counted from the registry at render, so the number stays true as entries change. Where a choice is required, Accept all and Reject all are the same component with different labels. Embedded media is never on by default, in any region.
06 · enforce
One writer, and withdrawal deletes
The app's own cookies are written by one module, which looks the name up in the registry and writes only if the visitor has granted that category. An unregistered name throws in development and quietly refuses in production. Withdrawing a category deletes every cookie and storage key the registry attributes to it and, for analytics, resets the tracking identity. The opt-out flag itself is classified strictly necessary and survives the sweep, so a no stays remembered.
07 · prove
Receipts carry a fingerprint
Each decision posts a receipt to an append-only Convex ledger: a random id, the basis, the three choices, edge-reported country and region, a query-stripped path and a timestamp. A change of mind adds a row rather than editing one. Every receipt carries an eight-character hash of the registry's consent-relevant facts. Add a vendor, recategorize an entry or lengthen a retention and the hash moves, so stored decisions stop counting and visitors are asked again.
// decisions
The parts that were a call, not a default — and what the alternative would have cost.
A consent registry in the codebase
A consent-management platform such as OneTrust, Cookiebot or Osano
Those platforms earn their keep on sites carrying dozens of ad-tech tags that only a scanner can find. Here every vendor is known at build time, so a scan would report less than the registry already states, and it would put a third-party script that sets its own cookies in front of every page. A list in code is also a list the build can check.
A version derived by hashing the registry
A version number someone remembers to bump
A rule enforced by a comment decays. The hash covers only the facts a visitor agreed to about each entry they could decline: what it is, its category, its provider and how long it lasts. Rewording a purpose, reordering the file or renaming a strictly necessary cookie leaves it alone, so a typo fix never re-prompts anyone. A new vendor always does.
Jurisdiction from edge headers, in one dynamic route
Trusting the browser, or reading geo in the root layout
A client that reports its own location can report anything. Reading request headers in the root layout would opt every marketing page out of static generation just to pick a banner variant. A small route, called only when no current decision exists, keeps the site static and costs returning visitors nothing.
Withdrawal that deletes
Withdrawal that only stops new writes
Stopping future writes leaves a year-long analytics identifier sitting in the browser. Clearing the category removes what it stored and resets the analytics identity, so a later re-grant starts a new person instead of resuming the old one. The opt-out flag deliberately survives: deleting the record of a no would mean asking again on the next page.
Receipts with no IP or user agent
A proof-of-consent record that fingerprints the browser
Storing a browser's fingerprint to prove it declined tracking would be tracking it. A receipt holds a random id, the choices, the edge-reported country and region, and a path with its query string stripped, because campaign links routinely carry an email address. It links to a person only if that browser later registers.
// guarantees
Nothing unlisted ships
The app's own writes route through registered writers, and the build fails on code that goes around them or names a key the registry does not cover.
Withdrawal deletes
Turning a category off removes everything the registry attributes to it, not just future writes. The opt-out flag stays, so a no is remembered.
Proof without tracking
Append-only receipts carry the registry fingerprint and edge-reported location. No IP, no user agent, no query string. A changed fingerprint asks again.