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. Policy, categories, and legal links live in apps/web/c15t.config.ts. The provider in consent.tsx resolves that config on the server.
  2. Leave NEXT_PUBLIC_C15T_BACKEND_URL unset for offline regional rules. Set it for a hosted consent.io or self-hosted backend.
  3. Analytics call useConsent("measurement") and pass that boolean 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_BACKEND_URL unsetDecisions stay on the device. EU opt-in, California opt-out, and no banner for the rest of the world
HostedNEXT_PUBLIC_C15T_BACKEND_URL set to your c15t or consent.io backendConsent is stored on the backend so you have an audit trail and the same decision across devices

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

Categories, legal links, translations, and the offline or hosted mode live in apps/web/c15t.config.ts. next.config.ts wraps the Next.js config with withConsentManifest from @c15t/nextjs/build so that file is part of the build.

Offline mode is the default. Hosted mode turns on when NEXT_PUBLIC_C15T_BACKEND_URL is set:

apps/web/c15t.config.ts
mode: hasHostedBackend
  ? undefined
  : offline({
      policyRules: [
        policyRulePresets.europeOptIn(),
        policyRulePresets.californiaOptOut(),
        policyRulePresets.worldOptOutNoPrompt(),
      ],
    }),

The provider resolves that config on the server and passes the active locale into the banner:

apps/web/src/lib/providers/consent.tsx
<ConsentRoot
  state={resolveConsent({ language: locale })}
  options={{
    i18n: {
      locale,
      detectBrowserLanguage: false,
    },
  }}
>
  {children}
  <ConsentBanner
    hideBranding
    legalLinks={["privacyPolicy", "cookiePolicy"]}
    layout={["customize", ["reject", "accept"]]}
  />
  <ConsentDialog hideBranding legalLinks={["privacyPolicy", "cookiePolicy"]} />
</ConsentRoot>

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_BACKEND_URL, the config uses offline mode and the three policy rules above. That matches typical GDPR (opt-in), CCPA (opt-out), and no banner elsewhere, 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_BACKEND_URL="https://your-instance.c15t.dev"

The value is validated as an optional URL in apps/web/env.config.ts. When it is present, c15t.config.ts leaves mode unset so c15t uses that backend.

Proxy the backend (optional)

For production, you can proxy the backend through your app so the browser never talks to the raw vendor host. Point NEXT_PUBLIC_C15T_BACKEND_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.

The hrefs are set in c15t.config.ts under options.legalLinks. Keep them aligned with the legal pages routes, including any locale prefix your app uses.

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

apps/web/src/lib/providers/analytics.tsx
const enabled = useConsent("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 useConsent("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

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

Banner width is set under options.theme in c15t.config.ts. hideBranding is already on the banner and dialog so the vendor badge stays off.

Locales

Consent copy uses @c15t/translations. The config imports English and Spanish messages and registers them on options.i18n.messages. When you add a locale to @workspace/i18n, add the matching c15t messages object in that 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 the offline policy rules (see c15t policy rules) to verify EU opt-in versus California opt-out without a VPN.

Verify the matrix

ScenarioExpected
Fresh visit from an EU region (offline rules)Banner shown; analytics off until Accept (or Customize, then enable measurement)
Reject alluseConsent("measurement") is false; analytics Provider is disabled and reset
Accept allAnalytics initializes; identify runs for signed-in users
Footer, Cookie preferencesDialog opens; changing measurement toggles analytics on or off
NEXT_PUBLIC_C15T_BACKEND_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 worldOptOutNoPrompt 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-* bridge in globals.css; theme attribute on html
Hosted mode errorsValid NEXT_PUBLIC_C15T_BACKEND_URL, CORS or 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 and pixels gated with useConsent(...) 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