Cookie consent
Add a configurable GDPR cookie consent banner that gates cookie-dropping analytics, chat, and affiliate scripts with smart auto detection by country.
The consent banner is configured in @repo/config and rendered client-side; config.cookieBanner governs whether it shows and which scripts wait on it. It accepts true, false, or "auto" (the default).
The three modes
config.cookieBanner is a single tri-state flag:
| Value | Behavior |
|---|---|
true | Banner always shows; consent-required scripts wait for it. |
false | Banner never shows; every script loads freely, no consent. |
"auto" (default) | Banner shows only when a cookie-dropping provider is enabled and the visitor is in a GDPR country. |
How "auto" decides
Under "auto" the banner surfaces only when both conditions hold:
- A cookie-dropping third party is enabled - consent-required analytics (
config.analytics.googleorconfig.analytics.posthog), Crisp support chat (config.support.crisp), or any affiliate tracker (config.affiliate.refgrow,affonso, orpromotekit). - The visitor must consent per
config.consentPolicy(default: GDPR countries plus undetected geo, resolved viaisGdprCountryfrom@repo/utils/helpers).
Two consequences follow:
- Privacy-focused analytics (
umami,plausible,openpanel,datafast,ahrefs,vercel) never drop cookies, so they never trigger the banner and never wait on consent. - Out of the box no cookie-dropping provider ships enabled, so the banner stays hidden. It appears for GDPR visitors once you enable one (Crisp, a consent-required analytics provider, or an affiliate tracker).
isGdprCountry fails safe - an unknown or unresolved location returns true, so under the default consentPolicy visitors whose country can't be detected still see the banner. Set consentPolicy: "gdpr-only" to auto-grant them instead.
Who must consent
config.consentPolicy decides which visitors must consent explicitly before the consent-required scripts run. The banner only ever shows to visitors who must consent; everyone else is granted automatically as soon as geo detection settles, so e.g. US visitors get full analytics without ever seeing a banner.
| Value | Behavior |
|---|---|
"gdpr-and-unknown" (default) | GDPR-country visitors and visitors whose country can't be detected must consent. Everyone else is granted automatically - fail-safe. |
"gdpr-only" | Only visitors detected in a GDPR country must consent. Visitors whose country can't be detected are granted automatically once detection settles. |
"everyone" | Every visitor must consent explicitly; nobody is granted automatically. |
"never" | Consent is implicit for everyone - the banner never shows and all analytics fire instantly (equivalent to cookieBanner: false). |
In development, an "auto" provider only counts toward the trigger when its enableInDev flag is set (config.analytics.enableInDev, config.support.enableInDev, config.affiliate.enableInDev). In production every enabled provider counts.
The acceptedCookies cookie
The visitor's choice persists in a tri-state acceptedCookies cookie:
| Cookie value | Meaning |
|---|---|
"true" | Consented - cookie-dropping scripts load. |
"false" | Declined - consent-required scripts stay off (Crisp still loads; see FAQ). |
| unset | No choice yet - banner shows if its surfacing conditions hold; consent-required scripts held back. |
Google Analytics implements this via Consent Mode v2: whenever a banner can be required, gtag.js starts with every consent signal defaulted to denied, so before (or without) consent it drops no cookies and sends only cookieless pings Google can model conversions from. A grant flips all four signals (including the ad_user_data / ad_personalization pair required for ads in the EEA) to granted.
The banner only renders once the component has hydrated client-side, so it never flashes during SSR.
Frequently asked questions
Does Crisp wait behind consent?
No. Crisp drops cookies so its presence surfaces the banner under "auto", but the widget itself loads as soon as it is configured - it is not blocked.
Why does the banner appear even with only privacy-focused analytics on?
It shouldn't from analytics alone - check for another cookie-dropping provider such as Crisp (config.support.crisp) or an affiliate tracker, which surface the banner. Remove that sub-key to drop the trigger.
Can I force the banner off everywhere?
Set config.cookieBanner to false (or config.consentPolicy to "never" - they are equivalent). All scripts then load freely with no consent gate - only do this if you have no cookie-dropping providers or operate outside consent-law jurisdictions.