Consent management for many categories — Klaro config + GA4 Consent Mode v2 + geo routing + cookie-less default
Consent management for many categories — Klaro config + GA4 Consent Mode v2 + geo routing + cookie-less default
Decision
The family's consent surface uses 5 categories × N services via Klaro, with three orthogonal levers stacked on top:
- Geo-routed defaults — EU/UK gets default-DENIED banner;
US/CA gets default-ACCEPTED with
Sec-GPChonoured; rest of world gets NO banner. - Lazy-loaded Klaro itself — Klaro JS ships ONLY to visitors
whose
CF-IPCountryis in the EU/UK/CCPA list. Other visitors get zero CLS, zero render-block, zero Klaro bytes. - Cookie-less defaults — every service that has a cookie-less mode uses it by default, so the consent surface stays small.
This refines, not supersedes,
security/cookie-banner-policy.md
— that policy stated "no banner unless EU + tracker"; this decision
adds the explicit category map, the US/CA + GPC handling, the lazy-
load rule, and the cookie-less default rule.
Categories (Klaro purposes array)
| Category | What it covers | Default consent |
|---|---|---|
necessary |
Auth session (Firebase Auth), CSRF tokens, session cookies, transactional notification routing | Always on, no consent UI shown |
analytics |
GA4, PostHog (autocapture mode), Microsoft Clarity, Sentry user-context, Algolia Insights | Geo-default (see Geo-routing below) |
marketing |
UTM persistence cookie, email-marketing UTM tracking, Razorpay cart UUID | Geo-default |
functional |
Theme preference, language preference, font-size preference, FCM push opt-in | Geo-default |
social |
Giscus comment cookie, Bluesky / AT Protocol auth tokens for lifestream embed | Off until user-clicked |
Services map (Klaro services array)
| Service | Category | Pre-consent posture | Notes |
|---|---|---|---|
| Cloudflare Web Analytics | necessary |
Loaded always | Cookie-less by design; documented under necessary for legal clarity |
| Sentry | necessary (default) ? analytics if user-PII captured |
Loaded with PII off by default | Default Sentry config does NOT capture PII; if a site flips on user-context, the entry moves to analytics for that site |
| Google Analytics 4 | analytics |
Loaded in denied mode via GA4 Consent Mode v2 |
Script loads, but tags fire only after gtag('consent', 'update', { analytics_storage: 'granted' }) |
| PostHog | analytics |
capture_pageview: false, persistence: 'memory' until consent |
After consent, persistence flips to localStorage; PostHog keeps anonymous mode by default per its service file |
| Microsoft Clarity | analytics + marketing |
Blocked until consent | Session-recording is the most sensitive surface; gated to both categories so denying either suppresses Clarity entirely |
| Knock | necessary |
Loaded always | Server-side transactional notifications; no client cookies |
| FCM (web push) | functional |
Browser permission prompt only after user clicks "Enable notifications" | Consent for FCM is the OS-level Notifications permission, not a Klaro toggle; the toggle just shows the in-app prompt button |
| Giscus | social |
Loaded only after consent OR user-click on a "Load comments" placeholder | Lazy-loaded iframe; placeholder visible until clicked |
| Algolia Insights | analytics |
Disabled until consent | Algolia search itself is necessary; the Insights events client is gated separately |
| UTM persistence cookie | marketing |
Set only after consent (EU); set immediately (US/CA pre-GPC); never set (rest, falls through to URL-only) | Documented in utm-attribution-strategy.md |
| Razorpay cart UUID | marketing |
Set on checkout-page entry; outside Klaro scope (necessary for the checkout flow) but flagged for legal clarity | Razorpay's own SDK manages this; the family doesn't override |
| Theme / language / font-size prefs | functional |
Set immediately, anywhere (low-sensitivity preferences) | Pre-existing user expectation that prefs persist; falls under "strictly necessary for the requested feature" |
Geo-routing rule
The CF edge reads CF-IPCountry on every request and emits a tiny
inline <script> that sets window.__consentRegime before any
tracker loads:
| Visitor region | __consentRegime |
Banner shown? | Default consent | Auto-honour |
|---|---|---|---|---|
| EU member state, UK, IS, NO, LI, CH | 'eu' |
Yes | DENIED for analytics / marketing / functional / social | — |
| US, CA | 'ccpa' |
Yes (CCPA "Do Not Sell" link required) | ACCEPTED | Sec-GPC: 1 request header ? auto-DENY analytics + marketing |
| Rest of world | 'rest' |
No banner | Trackers load if locally lawful (cookie-less default services always; cookie-issuing services per local law) | — |
Sec-GPC: 1 (Global Privacy Control) is honoured on every CCPA-
region request: when the header is present, Klaro pre-fills the
banner with analytics + marketing toggles OFF, equivalent to a CCPA
"Do Not Sell or Share" opt-out. The visitor can still re-enable via
the banner — GPC is the default, not a hard gate.
Lazy-load rule
Klaro's JS bundle (~17 KB gzipped from
jsDelivr) ships only to
visitors whose CF-IPCountry is in the union of EU + UK + Iceland +
Norway + Liechtenstein + Switzerland + US + CA. Other visitors:
- No Klaro JS download ? zero render-block.
- No banner DOM ? zero CLS.
- No
klaro-config.js? zero parse cost.
The CF edge emits a tiny inline <script> that conditionally injects
the Klaro <script> tag. The shared <ConsentBanner> helper in
@chirag127/oriz-kit (forward reference) ships this gate so no site
re-implements it.
This refines cookie-banner-policy.md:
that policy gated Klaro on (EU visitor) × (cookie-issuing tracker on
this page); this decision widens the visitor list to cover CCPA
regions and codifies the lazy-load JS-loading rule.
Cookie-less default rule
For services that have a cookie-less mode, USE IT by default:
| Service | Cookie-less mode | Default? |
|---|---|---|
| Cloudflare Web Analytics | Cookie-less by design | Yes |
| Sentry | sendDefaultPii: false (default) |
Yes |
| Cloudflare Pages basic auth (where used) | Token-based, no cookie | Yes |
| PostHog | persistence: 'memory' pre-consent |
Yes (until consent flips it) |
| reCAPTCHA Enterprise | Cookie-less assessment mode | Yes (Firestore-write surface only) |
| Cloudflare Turnstile | Cookie-less by design | Yes |
The smaller the cookie-issuing surface, the smaller the consent UI the family ever has to ship.
Why
- Many categories + many cookies is exactly Klaro's design point; it scales N-services × N-purposes without re-architecting.
- GA4 Consent Mode v2 is Google's official answer to "load the script, defer the firing" — pragmatic, doesn't require GA4 to fight the family's consent flow.
- Geo-routed defaults match law: EU is opt-in (GDPR), US/CA is opt-out (CCPA); ROW currently has no broad cookie-consent law that the family must defensively honour. This avoids the "banner-fatigue tax" on ~80% of visitors who legally don't need a banner.
- Lazy-loading Klaro itself means most pageviews ship zero consent JS — keeps Web Vitals high, keeps CLS at zero.
- Cookie-less by default means the categories the user has to consider are smaller in the first place; the more cookie-less services we use, the less work the consent surface does.
- All services in this map are free, no card — the family's rules continue to hold; consent management adds no cost surface.
Implications
- Single Klaro config lives in
@chirag127/oriz-kit(forward reference). Every site imports the sharedpurposes[]andservices[]arrays + a per-site override for which services actually load on that site. - GA4 Consent Mode v2 must be wired in the moment GA4 first ships on any site. Today GA4 isn't deployed; when it lands, the service file gets updated to reflect the consent-mode posture.
Sec-GPCparsing lives in the same edge inline script as the geo-routing logic — the CF edge passes it through; the inline script readsnavigator.globalPrivacyControl(the JS surface) + the response of a tiny header-echo Worker if needed.- Per-site rendering of the "Privacy Settings" footer link —
shown ONLY when at least one cookie-issuing service in
analytics/marketing/social is in the per-site service list AND the
visitor's
__consentRegime !== 'rest'. Sites with onlynecessaryservices (e.g. a static doc page) get no link. - CSP delta on Klaro pages — already documented in
services/business/security/klaro.md; the kit emits the additionalstyle-src 'unsafe-inline'only on pages where Klaro loads. - Microsoft Clarity is dual-categoried — denying either analytics OR marketing suppresses it; this is intentional caution given session-recording is the most sensitive class of capture.
- Giscus loads on user click even before consent if the visitor taps the "Load comments" placeholder — consent is implicit from the click; documented in the Giscus per-site placement.
- No third-party SaaS consent manager — CookieYes / Cookiebot /
Osano / OneTrust all rejected per
rules/infrastructure/no-subscriptions.mdand (in some cases) card-on-file requirement.
Cross-refs
- Klaro service entry
- Cookie banner policy (refined by this decision)
- Cloudflare Web Analytics — cookie-less primary signal
- PostHog — cookie-issuing in identified mode
- Microsoft Clarity — session recording
- Knock — transactional notifications, server-side
- FCM — web push, functional category
- Security headers strategy — CSP coupling
- UTM attribution strategy — marketing-cookie context
- No card-on-file rule
- No subscriptions rule
- Never hit quotas rule