How to implement Consent Mode v2 as a product control

I
By Inth EngineeringMember of Technical Staff
Guides7 min read

Implement Google Consent Mode v2 with denied defaults, clear consent mapping, update calls, and consent records your team can review later.

Google Consent Mode v2 is required for EU traffic. Here's what developers actually need to know.

If you've received an email from Google about "Consent Mode v2", you're not alone. It is one of those things that gets explained with far more jargon than it needs.

Here is the simple version: Consent Mode v2 is just a way to tell Google what the user has actually consented to, before Google starts doing anything with that data.

If you are using Google Ads, GA4, or Google Tag Manager for visitors in the EEA, you need to get this right. Not eventually. Up front.

What Consent Mode v2 Actually Is

Consent Mode is Google's way of adjusting behaviour based on consent state instead of pretending every visitor should be tracked the same way.

Consent Mode v1 had two signals:

  • ad_storage: Can Google store advertising-related data?
  • analytics_storage: Can Google store analytics-related data?

Consent Mode v2 adds two more:

  • ad_user_data: Can user data be sent to Google for advertising use?
  • ad_personalization: Can that data be used for personalised ads?

That is the whole model. Four values. Granted or denied.

Why Developers Should Care

Without a proper Consent Mode v2 setup, Google measurement and advertising get messy fast. You can end up with:

  • broken or partial conversion tracking
  • unreliable attribution
  • remarketing issues
  • tags firing in the wrong state
  • consent signals arriving too late to be useful

This is not really a marketing problem. It is an ordering problem.

The important part is not just sending consent updates. It is making sure the default consent state exists early enough that Google tools do not make assumptions before the user has made a choice.

The Part Most Guides Skip

Most Consent Mode explanations make it sound like you need to hand-roll gtag('consent', ...) calls everywhere.

You usually do not.

With current c15t Next.js setup, c15t from Inth gives you a ConsentManagerProvider at the app root, handles the initialization flow, reads existing consent, resolves policy and location, and decides whether the banner should show before the first meaningful consent-aware render.

That is the real job.

How c15t Handles Consent Mode v2 Today

The current model is simpler than older blog posts and examples make it look.

1. The consent runtime starts early

When the provider mounts, c15t creates the consent runtime, checks stored preferences, fetches resolved policy data in hosted mode, and applies gating rules immediately. That lifecycle is documented in Initialization Flow.

In practice, that means scripts, iframes, and network calls that depend on consent can be blocked until the right category is granted.

2. Google integrations start with denied defaults

With the current GA4 + Google Ads integration and Google Tag Manager integration, c15t initializes Google with Consent Mode v2 defaults set to denied, then sends updates when the user makes a choice.

That matters because it lets Google libraries load safely without treating silence as consent.

3. Consent changes are pushed automatically

When a user accepts, rejects, or changes preferences later, c15t updates the consent state for you. You are not wiring this up manually across every route and event.

The Current Next.js Setup

If you are starting fresh, the current Next.js quickstart looks more like this:

tsx
// components/consent-manager/provider.tsx 'use client'; import { type ReactNode } from 'react'; import { ConsentManagerProvider, ConsentBanner, ConsentDialog, } from '@c15t/nextjs'; export default function ConsentManagerClient({ children }: { children: ReactNode }) { return ( <ConsentManagerProvider options={{ mode: 'hosted', backendURL: 'https://your-instance.c15t.dev', consentCategories: ['necessary', 'measurement', 'marketing'], }} > <ConsentBanner /> <ConsentDialog /> {children} </ConsentManagerProvider> ); }

Then mount it at the app root:

tsx
// app/layout.tsx import { ConsentManager } from '@/components/consent-manager'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <ConsentManager>{children}</ConsentManager> </body> </html> ); }

That gets the consent system in place. Then you add Google integration based on how your stack is set up.

If You Use gtag.js Directly

If you are wiring GA4 or Google Ads directly in code, use the GA4 + Google Ads integration:

tsx
import { type ReactNode } from 'react'; import { ConsentManagerProvider } from '@c15t/react'; import { gtag } from '@c15t/scripts/google-tag'; const scripts = [ gtag({ id: 'G-XXXXXXXXXX', category: 'measurement', }), ]; export function ConsentProvider({ children }: { children: ReactNode }) { return ( <ConsentManagerProvider options={{ mode: 'hosted', backendURL: 'https://your-instance.c15t.dev', scripts, }} > {children} </ConsentManagerProvider> ); }

The important detail is this: file gtag.js can load from page start because c15t sets Consent Mode defaults to denied first. When consent changes, c15t pushes the update automatically.

So yes, window.gtag?.('event', 'sign_up') is safe to call, because the Google SDK suppresses transmission while the relevant consent is denied.

If You Use Google Tag Manager

If your team manages tags in GTM, use the Google Tag Manager integration:

tsx
import { type ReactNode } from 'react'; import { ConsentManagerProvider } from '@c15t/react'; import { googleTagManager } from '@c15t/scripts/google-tag-manager'; const scripts = [googleTagManager({ id: 'GTM-XXXXXXX' })]; export function ConsentProvider({ children }: { children: ReactNode }) { return ( <ConsentManagerProvider options={{ mode: 'hosted', backendURL: 'https://your-instance.c15t.dev', scripts, }} > {children} </ConsentManagerProvider> ); }

This is one place where developers often get confused.

GTM itself can load from the start. That does not mean your marketing tags are free to fire. c15t sets denied defaults first, GTM maintains its internal consent state, and GTM-managed tags only run when the matching consent has been granted.

You will usually also want to create a custom consent-update trigger inside GTM so existing tags re-evaluate when preferences change.

Common Mistakes to Avoid

1. Mixing old and new c15t examples

A lot of older examples use older package names or config shapes. If you are following the current docs, start with @c15t/nextjs for app setup and the current integrations overview.

2. Loading both GTM and gtag.js for the same destination

The docs are clear on this: use GTM if you want central tag management, or file gtag.js if you only need GA4 or Google Ads directly. Running both for the same destination can duplicate events.

3. Treating analytics and marketing as the same thing

c15t has explicit consent categories. Use measurement for analytics and marketing for advertising or conversion tracking. Do not collapse everything into one bucket because it feels quicker.

4. Ignoring policy resolution and re-prompting

Consent is not just "show banner once". The current policy pack model supports jurisdiction-aware policy resolution and re-prompting when materially relevant policy decisions change.

5. Forgetting the backend tradeoff

You can run c15t in offline mode, but the client modes docs are pretty direct about the tradeoff: you give up backend audit history, server-side awareness, and automatic jurisdiction detection. For most real production setups, hosted mode is the better default.

How to Verify It Is Working

A practical test is better than staring at config for an hour.

For GTM

Use GTM Preview and check:

  1. the container loads on page load
  2. non-essential tags do not fire before consent
  3. a consent-update event appears after the user makes a choice
  4. measurement or marketing tags only fire after the matching consent is granted
  5. revoking consent produces another update and stops affected tags

For gtag.js

With the Google Tag integration, check that Google is initialized with denied defaults first, then updated after consent is granted. If events are being called before consent, that is fine. They should still be suppressed until the relevant category is allowed.

For the overall consent lifecycle

Use DevTools while building and debugging, and review the Initialization Flow if banner timing or state changes seem odd.

The Bottom Line

Consent Mode v2 is not hard because the model is complicated.

It is hard because timing, state, and script behaviour all need to line up.

c15t handles the awkward parts without adding much weight to the page itself. c15t loads in 89ms, so you are not trading consent correctness for a heavy banner.

c15t handles the awkward parts:

  • initializing consent state early
  • setting denied defaults for Google integrations
  • updating consent automatically when users change preferences
  • gating scripts and related behaviour by category
  • supporting hosted, jurisdiction-aware consent flows in production

So the developer job is much simpler than it used to be: mount the provider correctly, choose the right Google integration, map your categories sensibly, and verify the behaviour in preview tools.

That is it.

Want to hand this to your agent to deploy? Sign up to Inth, copy the prompt on sign up, and let your agent wire c15t into your repo against the matching docs. Then review the diff and ship.

#Consent#Analytics#Next.js