Cookie Consent (DPDP 2023)¶
A cookie-consent banner ships across apps/site and apps/web via the shared packages/consent package. It gates PostHog (analytics, replay, and error handlers via installGlobalErrorHandlers) and Google Fonts behind explicit opt-in. The default state is no consent: no tracking, no Google Fonts, system font stack only. Copy uses DPDP vocabulary (Data Principal, purpose-by-purpose opt-in) via t(), English live and Hindi first-pass. Kwilo doesn't use Sentry; PostHog handles errors, so gating PostHog gates error capture.
How it works¶
User visit
│
├── Cookie present → read consent state → apply (init or skip PostHog; load or skip Google Fonts)
│
└── No cookie → show banner
├── Accept all → set cookie, init PostHog, attach error handlers, load Google Fonts
├── Customise → set cookie per purpose, init what was accepted
└── Reject all → set cookie (all false), system fonts, no PostHog
The banner anchors to the bottom of the viewport on apps/web (logged-in app) and shows as a center modal on apps/site (marketing). It's non-blocking: ignoring it leaves the default no-consent state. A "Manage cookies" footer link reopens the preference modal on both apps. Toggling Analytics off later calls posthog.opt_out_capturing(); replay stops on the next pageview.
Purposes¶
| Purpose | Locked | What it gates |
|---|---|---|
| Necessary | on | session cookie, CSRF, auth. Never gated |
| Analytics | opt-in | PostHog initAnalytics, installGlobalErrorHandlers, replay |
| Preferences | opt-in | Google Fonts (Manrope, Fraunces); pre-consent uses the system stack |
Behavior-affecting decisions¶
- Default off, opt-in. DPDP treats consent as an affirmative action, so nothing non-Necessary runs until accepted. This is also accidentally GDPR-compliant for EU traffic on
kwilo.ai. - Gate
initAnalytics, don't init-then-opt-out. PostHog isn't initialised until consent is granted. Initialising and callingopt_out_capturingafter the fact still writes theph_kwilocookie to disk, which fails an InfoSec audit.initAnalyticsis idempotent, so re-granting is a no-op. - Google Fonts injected at runtime. The static
<link rel="preconnect">andfonts.googleapis.comtags are removed from bothindex.htmlfiles and injected via JS only when the Preferences purpose is true. Pre-consent CSS uses the@kwilo/uisystem font stack. - Stale cookie schema. A missing purpose key in stored consent is treated as not consented; the banner reshows on a schema bump.
CMP¶
c15t 2.1.0 (@c15t/react + c15t, Apache-2.0, headless), pinned in packages/consent/package.json. Headless means the banner and preference modal are built from @kwilo/ui primitives (Dialog, Button, Switch) so the CMP belongs to the product visually. If the CMP script fails to load, a 1.5s timeout falls back to no consent.
Where it lives¶
- Banner is global on every route of
kwilo.aiandapp.kwilo.ai;/privacyand/cookie-policyonapps/sitecarry the DPO contact, purpose table, and withdrawal steps. packages/consent/src/:index.ts(barrel:ConsentProvider,useConsent,injectGoogleFonts,removeGoogleFonts),ConsentProvider.tsx,useConsent.ts(maps c15tconsents.measurement→analytics,consents.functionality→functional),fonts.ts,copy.ts,constants.ts.apps/web/src/main.tsx+apps/site/src/App.tsx: gate analytics + error handlers onuseConsent().analytics, fonts on.functional.- "Manage cookies" triggers:
apps/web/.../ManageCookiesLink.tsx,apps/site/.../LandingFooter.tsx.
The consent cookie cc_kwilo is stored on .kwilo.ai (cross-subdomain, SameSite=Lax; Secure):