Get in touch
All systems

// teardown

The consent registry

The cookie policy is the code.

live·2026
Next.js 16TypeScriptConvexVercel edge geolocationPostHog

Source of truth

Consent registry
Build check
Cookie policy
↓ categories · allowlist

Every visit

Edge posture
Consent banner
Single writer
Receipt ledger

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.

chose

A consent registry in the codebase

not

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.

chose

A version derived by hashing the registry

not

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.

chose

Jurisdiction from edge headers, in one dynamic route

not

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.

chose

Withdrawal that deletes

not

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.

chose

Receipts with no IP or user agent

not

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.

// by the numbers

22
registered cookie and storage entries
1
list behind enforcement, banner and policy
0
IPs or user agents on a receipt

What it did

Ryse case study

// let's talk

Let's build something.

Have a product that needs both sharp strategy and someone who can ship it? I'd love to hear about it.

Get in touch

Command Palette

Search for a command to run...