GenerateSaaS

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:

ValueBehavior
trueBanner always shows; consent-required scripts wait for it.
falseBanner 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.google or config.analytics.posthog), Crisp support chat (config.support.crisp), or any affiliate tracker (config.affiliate.refgrow, affonso, or promotekit).
  • The visitor must consent per config.consentPolicy (default: GDPR countries plus undetected geo, resolved via isGdprCountry from @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.

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.

ValueBehavior
"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 visitor's choice persists in a tri-state acceptedCookies cookie:

Cookie valueMeaning
"true"Consented - cookie-dropping scripts load.
"false"Declined - consent-required scripts stay off (Crisp still loads; see FAQ).
unsetNo choice yet - banner shows if its surfacing conditions hold; consent-required scripts held back.

Google Analytics is gated load-on-grant: gtag.js does not load at all until consent is granted, so nothing pings or drops cookies beforehand. Grants carry the full Consent Mode v2 signal set, including the ad_user_data / ad_personalization pair required for ads in the EEA.

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.

On this page