Cookie Consent
Cookie Consent
GDPR-style consent banner via vanilla-cookieconsent, rendered globally and toggled
at runtime through a feature flag, no redeploy needed to turn it on or off.
How it works
CookieConsentBannerlives atsrc/components/cookie-consent/CookieConsentBanner.tsxand is mounted once insrc/routes/__root.tsx, so it covers every route.- The component reads the
cookie_consent_enabledfeature flag viauseFeatureFlag. When the flag is off, the banner itself doesn't render, but the Umami and Clarity scripts (if configured) still load unconditionally on mount, an explicit admin opt-out of consent gating, not a no-op. Toggling happens live at/dashboard/admin/feature-flags. - When enabled,
CookieConsent.run()shows a bottom-right consent modal with two categories:necessary, enabled and read-only (session cookies, CSRF, consent cookie itself)analytics, opt-in, off by default
- Consent state persists in the
cc_cookiecookie for 365 days.revision: 1is the consent version: bump it when your cookie usage materially changes and every user is re-prompted. - The modal copy interpolates
config.appNameand links/privacyand/tos.
Why a feature flag instead of an env var
Consent requirements depend on where your users are, not on your deploy pipeline. The
flag lets an admin enable the banner the day it becomes relevant (EU launch, legal
review) without a build. Seed the flag in scripts/seed.ts (FLAGS array) and apply
with make db.seed, idempotent.
Wiring analytics to consent
PostHog, Umami and Clarity are loaded by loadAnalytics() from
src/lib/analytics/ (one provider each, see
Product analytics), and CookieConsentBanner
is its only caller, so nothing is injected statically in __root.tsx. When the
flag is on, onConsent/onChange callbacks check
CookieConsent.acceptedCategory('analytics') and only then call loadAnalytics();
rejecting after a prior accept clears umami.*, Clarity's (_clck, _clsk,
CLID) and PostHog's (ph_*) cookies and reloads the page (via the analytics
category's autoClear config) so no tracker keeps running. PostHog is set to
cookie persistence for exactly this reason: autoClear can't delete localStorage.
When the flag is off, all of them load unconditionally on mount, an explicit
admin opt-out of the consent requirement, not a gap.
The banner waits for the flag query to resolve before doing either.
useFeatureFlag reports enabled: false while loading, and acting on that once
loaded every tracker before consent on sites that had the banner turned on.
Any future analytics/tracking script gets a provider in
src/lib/analytics/providers.ts, not a script tag injected elsewhere.
Customizing
All copy, categories, layout, and cookie lifetime live inline in
CookieConsentBanner.tsx (single-file component, no abstraction). Edit translations
under language.translations.en, add categories under categories, and mirror them
in the preferences modal sections.