Cookie Consent System
A GDPR and CCPA cookie consent system I built as a shared package and installed across several Next.js sites, with a serverless consent-logging API and a per-site cookie profile that decides whether a banner renders at all.

Case Study
The problem
I maintain several Next.js sites, and every one of them eventually needs the same thing: a way to ask for cookie consent that holds up. It needs more than a banner. It needs a mechanism that blocks non-essential scripts until the visitor opts in, offers granular categories, gives reject the same weight as accept, lets someone withdraw later, and keeps a provable record of every decision. Re-implementing that per site meant duplicated code and slow drift between installs, and reaching for a hosted CMP meant adding a third-party script to pages I had worked to keep first-party, plus a per-site subscription for what is nearly identical logic. So I built it once, as a shared package I install everywhere. I wrote up the build-versus-buy reasoning separately; this is the engineering side.
Shape of the package
The core is a TypeScript package bundled with Rollup and installed straight from GitHub, so there is no private registry to run. It exposes a React context provider that owns the parts that should never be re-implemented per site: consent state, reading and writing stored preferences, gating analytics behind those preferences, and logging each decision. A small CLI scaffolds the visible pieces (the banner, the preferences modal, the cookie policy page) into a project as ordinary components rather than locking them behind a configured widget. That was deliberate: each site has its own Tailwind and shadcn tokens, and I wanted to restyle the UI per site without fighting a vendor's markup. The provider stays framework-agnostic by taking the analytics implementation and the logging config as injected props, so it never hard-codes GA4 or any specific backend.
The cookie profile decides everything
The design decision I like most is small. Each site declares a cookie profile: which categories it uses, out of essential, analytics, and marketing. That profile is the single source of truth. The banner, the preferences modal, the consent logging, and the policy page all derive from it. If a site only sets essential cookies, no banner renders, because there is nothing to consent to, and the compliance log is skipped because no decision is being made. When a site later adds Google Analytics, I flip one flag and the banner, the analytics toggle, and the matching policy section all appear together. The "Accept all" action only ever enables the categories the profile declares, so it cannot silently switch on a category the site does not use. I added the profile after the first version shipped, which is why omitting it defaults to all categories on for backward compatibility. If I were starting over I would make the profile required from day one.
The logging backend and its security model
Consent events are written by a serverless endpoint on Vercel backed by Neon Postgres. Logging is fire-and-forget: by the time the request runs, localStorage is already the source of truth, so the client never blocks the UI on it. A failed send retries once and then buffers in a capped localStorage queue that flushes on the next provider mount, so a dropped request does not cost an audit record. The security model matters here because the repository is public, which was a choice: it is how I install the package without a registry. Everything is built on the assumption that the source is readable. The consent endpoint uses a per-domain key that ships to the browser, and I treat it as public: it only attributes an event to a domain and can do nothing destructive. The real protections are a CORS origin allowlist and rate limiting, both driven from a database table, so registering a new site is a row insert with no code change and no redeploy. The destructive operation, GDPR erasure, sits behind a separate server-only admin secret that never reaches a browser, checked with a constant-time comparison, and its rate limiter fails closed so an outage denies deletions rather than allowing them. IP addresses are hashed with a server-side salt before storage.
Where it stops, and what I would revisit
Building this in-house means I own the maintenance as the law moves, and it does: the UK's Data (Use and Access) Act changed analytics-consent rules in 2025, the IAB framework keeps versioning, and Global Privacy Control is now mandatory in California. I also drew a deliberate scope line. The package does not implement the IAB Transparency and Consent Framework or Google Consent Mode v2, so it is not the right tool for a site monetizing with Google advertising in the EEA, where a Google-certified CMP is required. It is built for first-party sites that need clean opt-in consent and an audit trail, which is most of what I run. The next thing I would add is Global Privacy Control signal handling, since that generalizes across sites and is already required in California.