For the complete documentation index, see llms.txt. Prefer markdown by appending .md to documentation URLs or sending Accept: text/markdown.

Cookie consent

Configure TurboStarter web cookie consent with c15t, choose offline or hosted mode, and gate analytics on measurement consent.

TurboStarter ships cookie consent for the web app with c15t. The banner and preferences dialog are already wired into the provider tree, themed to your design tokens, and connected so analytics only run after the user grants measurement consent.

You do not need to install another CMP for the happy path - customize categories, legal links, and storage mode in one place.

TL;DR

  1. Consent lives in apps/web/src/lib/providers/consent.tsx and wraps analytics in providers.tsx.
  2. Leave NEXT_PUBLIC_C15T_URL unset for offline regional policy packs; set it for hosted consent.io / self-hosted backend.
  3. Analytics call has("measurement") and pass enabled into @workspace/analytics-web Provider.
  4. Users reopen preferences from the footer via ConsentDialogLink.
  5. Keep privacy and cookie policy pages filled - the banner links to them.

Offline vs hosted

ModeWhenBehavior
Offline (default)NEXT_PUBLIC_C15T_URL unsetClient-side storage + regional policy packs: EU opt-in, California opt-out, rest of world no banner
HostedNEXT_PUBLIC_C15T_URL set to your c15t / consent.io backendConsent persisted on the backend for audits and multi-device consistency

Use offline for local development and simple sites. Prefer hosted when you need a durable audit trail or a managed geo policy from c15t.

Inspect the ConsentProvider

The kit wraps the app with ConsentManagerProvider, renders the banner and dialog, and passes the active locale into c15t i18n:

apps/web/src/lib/providers/consent.tsx
<ConsentManagerProvider
  options={{
    ...(env.NEXT_PUBLIC_C15T_URL
      ? { mode: "hosted" as const, backendURL: env.NEXT_PUBLIC_C15T_URL }
      : {
          mode: "offline" as const,
          offlinePolicy: {
            policyPacks: [
              policyPackPresets.europeOptIn(),
              policyPackPresets.californiaOptOut(),
              policyPackPresets.worldNoBanner(),
            ],
          },
        }),
    consentCategories: ["necessary", "measurement"],
    legalLinks: {
      privacyPolicy: {
        href: pathsConfig.marketing.legal("privacy-policy"),
        target: "_self",
      },
      cookiePolicy: {
        href: pathsConfig.marketing.legal("cookie-policy"),
        target: "_self",
      },
    },
    // i18n + theme…
  }}
>
  <ConsentBanner
    hideBranding
    legalLinks={["privacyPolicy", "cookiePolicy"]}
    layout={["customize", ["reject", "accept"]]}
  />
  <ConsentDialog hideBranding legalLinks={["privacyPolicy", "cookiePolicy"]} />
  {children}
</ConsentManagerProvider>

ConsentProvider sits outside AnalyticsProvider in providers.tsx so analytics can read consent state on first paint.

Choose offline or hosted storage

Offline (default)

Do nothing. Without NEXT_PUBLIC_C15T_URL, the provider uses offline mode and the three policy packs above. That matches typical GDPR (opt-in), CCPA (opt-out), and “no banner elsewhere” expectations without a backend.

Hosted

Create a consent.io (or self-hosted c15t) instance, then set the public backend URL in apps/web/.env.local and your deployment env:

apps/web/.env.local
NEXT_PUBLIC_C15T_URL="https://your-instance.c15t.dev"

The value is validated as an optional URL in apps/web/env.config.ts. When it is present, the provider switches to mode: "hosted" and uses that URL as backendURL.

Proxy the backend (optional)

For production, c15t recommends proxying the backend through your app (for example a Next.js rewrite to /api/c15t) so the browser never talks to the raw vendor host. Point NEXT_PUBLIC_C15T_URL at that same-origin path when you add the rewrite.

The banner and dialog link to:

  • /legal/privacy-policy
  • /legal/cookie-policy

Those routes come from the legal pages CMS collection. Fill the MDX content before launch - empty policies with live consent UI still look unfinished to users and reviewers.

Paths are built with pathsConfig.marketing.legal(...) so locale prefixes stay correct.

AnalyticsProvider already checks the measurement category before identifying users or enabling the analytics Provider:

apps/web/src/lib/providers/analytics.tsx
const { has } = useConsentManager();
const enabled = has("measurement");

useEffect(() => {
  if (session.isPending) {
    return;
  }

  if (!enabled) {
    return reset();
  }

  if (session.data?.user) {
    const { id, email, name } = session.data.user;
    identify(id, { email, name });
  } else {
    reset();
  }
}, [session, enabled]);

return <Provider enabled={enabled}>{children}</Provider>;

Each web analytics strategy honors enabled (for example PostHog opts out of capturing when it is false). When you add a custom analytics provider, accept enabled?: boolean on Provider and skip init / page views when it is false - see analytics tracking.

If you introduce marketing pixels later, add a marketing category to consentCategories and gate those scripts with has("marketing") the same way.

Let users reopen preferences

The marketing footer includes a Cookie preferences entry that opens the consent dialog without a separate route:

apps/web/src/modules/marketing/layout/footer/navigation.tsx
{
  title: "legal.cookies",
  component: ConsentDialogLink,
},

Copy lives under common:legal.cookies (Cookie preferences in English). Keep that link in any custom footer so users can revoke or change consent after the first choice.

Customize branding, locale, and categories

Theme

c15t styles import from @c15t/nextjs/styles.css. TurboStarter maps c15t CSS variables to your theme tokens on html:root[data-theme] in apps/web/src/assets/styles/globals.css (--c15t-primary--primary, surfaces, radii, and so on). Change the design system theme and the banner follows.

Action button variants and banner max-width are set under options.theme in consent.tsx. hideBranding is already on so the vendor badge stays off.

Locales

Consent copy uses @c15t/translations for the locales you register. The kit maps Locale.EN / Locale.ES to baseTranslations. When you add a locale to @workspace/i18n, add the matching c15t messages object in the same i18n.messages map.

detectBrowserLanguage is false so the UI locale stays tied to the Next.js [locale] segment.

Categories

Default:

consentCategories: ["necessary", "measurement"];

Extend only when you actually load scripts in that bucket (marketing, functional, and so on). Updating the array without gating new scripts leaves toggles that do nothing.

Preview a region offline

During local testing you can force a country override in offline policy options (see c15t policy packs) to verify EU opt-in vs California opt-out without a VPN.

Verify the matrix

ScenarioExpected
Fresh visit from an EU region (offline packs)Banner shown; analytics off until Accept (or Customize → enable measurement)
Reject allhas("measurement") false; analytics Provider disabled / reset
Accept allAnalytics initializes; identify runs for signed-in users
Footer → Cookie preferencesDialog opens; changing measurement toggles analytics on/off
NEXT_PUBLIC_C15T_URL setHosted mode; decisions go to your c15t backend
Legal links in bannerNavigate to privacy and cookie policy pages

Also confirm in DevTools that your analytics network calls only appear after measurement consent.

Troubleshooting

SymptomWhat to check
Banner never appearsOffline worldNoBanner for non-EU/CA geos; hosted geo rules; browser already stored a prior decision - clear site data
Analytics still fire after rejectConfirm ConsentProvider wraps AnalyticsProvider; custom providers must respect enabled
Banner looks unstyled / wrong colors@c15t/nextjs/styles.css import and --c15t-* bridge in globals.css; theme attribute on html
Hosted mode errorsValid NEXT_PUBLIC_C15T_URL, CORS / rewrite to the backend, env present in the client build
Wrong language in dialogLocale passed into ConsentProvider; messages map includes that Locale key

Checklist

  • Privacy and cookie policy MDX filled in
  • Offline packs acceptable for your markets, or hosted URL configured
  • Only real tracking categories listed in consentCategories
  • New analytics / pixels gated with has(...) or enabled
  • Footer (or settings) still exposes cookie preferences
  • Spot-check Accept / Reject / Customize in the regions you care about

How is this guide?

Last updated on

On this page

Ship your startup everywhere. In minutes.Try TurboStarter