type: decision
status: active
timestamp: 2026-06-20
tags: [security, privacy, consent, klaro, gdpr, ccpa, gpc, cookies, multi-category, geo]

Consent management for many categories — Klaro config + GA4 Consent Mode v2 + geo routing + cookie-less default

Klaro consent: 5 categories. EU/UK denied, US/CA accepted

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:

  1. Geo-routed defaults — EU/UK gets default-DENIED banner; US/CA gets default-ACCEPTED with Sec-GPC honoured; rest of world gets NO banner.
  2. Lazy-loaded Klaro itself — Klaro JS ships ONLY to visitors whose CF-IPCountry is in the EU/UK/CCPA list. Other visitors get zero CLS, zero render-block, zero Klaro bytes.
  3. 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)

CategoryWhat it coversDefault consent
necessaryAuth session (Firebase Auth), CSRF tokens, session cookies, transactional notification routingAlways on, no consent UI shown
analyticsGA4, PostHog (autocapture mode), Microsoft Clarity, Sentry user-context, Algolia InsightsGeo-default (see Geo-routing below)
marketingUTM persistence cookie, email-marketing UTM tracking, Razorpay cart UUIDGeo-default
functionalTheme preference, language preference, font-size preference, FCM push opt-inGeo-default
socialGiscus comment cookie, Bluesky / AT Protocol auth tokens for lifestream embedOff until user-clicked

Services map (Klaro services array)

ServiceCategoryPre-consent postureNotes
Cloudflare Web AnalyticsnecessaryLoaded alwaysCookie-less by design; documented under necessary for legal clarity
Sentrynecessary (default) ? analytics if user-PII capturedLoaded with PII off by defaultDefault Sentry config does NOT capture PII; if a site flips on user-context, the entry moves to analytics for that site
Google Analytics 4analyticsLoaded in denied mode via GA4 Consent Mode v2Script loads, but tags fire only after gtag('consent', 'update', { analytics_storage: 'granted' })
PostHoganalyticscapture_pageview: false, persistence: 'memory' until consentAfter consent, persistence flips to localStorage; PostHog keeps anonymous mode by default per its service file
Microsoft Clarityanalytics + marketingBlocked until consentSession-recording is the most sensitive surface; gated to both categories so denying either suppresses Clarity entirely
KnocknecessaryLoaded alwaysServer-side transactional notifications; no client cookies
FCM (web push)functionalBrowser 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
GiscussocialLoaded only after consent OR user-click on a “Load comments” placeholderLazy-loaded iframe; placeholder visible until clicked
Algolia InsightsanalyticsDisabled until consentAlgolia search itself is necessary; the Insights events client is gated separately
UTM persistence cookiemarketingSet 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 UUIDmarketingSet on checkout-page entry; outside Klaro scope (necessary for the checkout flow) but flagged for legal clarityRazorpay’s own SDK manages this; the family doesn’t override
Theme / language / font-size prefsfunctionalSet 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__consentRegimeBanner shown?Default consentAuto-honour
EU member state, UK, IS, NO, LI, CH'eu'YesDENIED for analytics / marketing / functional / social
US, CA'ccpa'Yes (CCPA “Do Not Sell” link required)ACCEPTEDSec-GPC: 1 request header ? auto-DENY analytics + marketing
Rest of world'rest'No bannerTrackers 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:

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.

For services that have a cookie-less mode, USE IT by default:

ServiceCookie-less modeDefault?
Cloudflare Web AnalyticsCookie-less by designYes
SentrysendDefaultPii: false (default)Yes
Cloudflare Pages basic auth (where used)Token-based, no cookieYes
PostHogpersistence: 'memory' pre-consentYes (until consent flips it)
reCAPTCHA EnterpriseCookie-less assessment modeYes (Firestore-write surface only)
Cloudflare TurnstileCookie-less by designYes

The smaller the cookie-issuing surface, the smaller the consent UI the family ever has to ship.

Why

Implications

Cross-refs


Edit on GitHub · Back to index