Use Consent Manager

useConsentManager

useConsentManager() is the primary hook for interacting with the consent system. It returns the complete consent store state and all action methods.

import { useConsentManager } from '@c15t/nextjs';

function MyComponent() {
  const {
    consents,
    model,
    has,
    saveConsents,
    // ... all state and actions
  } = useConsentManager();
}

State Properties

Warning: ExtractedTypeTable: Could not extract "StoreRuntimeState" from "./packages/core/src/store/type.ts" using base path "/vercel/path0/apps/c15t-docs/.leadtype/c15t". Verify the path/name and that the file is included by your tsconfig.

Loading…

Action Methods

Warning: ExtractedTypeTable: Could not extract "StoreActions" from "./packages/core/src/store/type.ts" using base path "/vercel/path0/apps/c15t-docs/.leadtype/c15t". Verify the path/name and that the file is included by your tsconfig.

Loading…

User Identification

You can link a consent subject to your own internal user ID or a non-secret anonymous visitor ID. c15t stores the identifier in the subject row's externalId field. Consent records remain associated with that subject through subjectId, so your backend can find the subject and its related consent records by external ID for GDPR Article 15 exports.

1. Providing the Current User ID

When using Clerk, prefer the Clerk userId whenever the visitor is signed in. Before sign-in, you can fall back to a non-secret visitor ID stored in a first-party cookie:

Info

Clerk's auth() helper requires clerkMiddleware() to be configured. This example also uses the asynchronous cookies() API from Next.js 15+. For Next.js 14 and earlier, use const cookieStore = cookies(); without await. Keep the layout asynchronous because Clerk's auth() is asynchronous.

// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';
import { auth } from '@clerk/nextjs/server';
import { ConsentManagerProvider } from '@c15t/nextjs';
import { cookies } from 'next/headers';
import { ClerkConsentSync } from './clerk-consent-sync';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const { userId } = await auth();
  const cookieStore = await cookies();
  const visitorId = cookieStore.get('consent_visitor_id')?.value;
  const consentUser = userId
    ? { id: userId, identityProvider: 'clerk' }
    : visitorId
      ? { id: visitorId, identityProvider: 'anonymous-visitor' }
      : undefined;

  return (
    <html lang="en">
      <body>
        <ClerkProvider>
          <ConsentManagerProvider
            options={{
              mode: 'hosted',
              backendURL: '/api/c15t',
              consentCategories: ['necessary', 'measurement', 'marketing'],
              user: consentUser,
            }}
          >
            <ClerkConsentSync />
            {children}
          </ConsentManagerProvider>
        </ClerkProvider>
      </body>
    </html>
  );
}

2. Synchronizing Clerk Sign-ins

Clerk can complete a sign-in without reloading the page. Add a client component inside ConsentManagerProvider to update the current consent subject as soon as Clerk exposes the authenticated userId:

// app/clerk-consent-sync.tsx
'use client';

import { useAuth } from '@clerk/nextjs';
import { useConsentManager } from '@c15t/nextjs';
import { useEffect } from 'react';

export function ClerkConsentSync() {
  const { isLoaded, isSignedIn, userId } = useAuth();
  const { identifyUser } = useConsentManager();

  useEffect(() => {
    if (!isLoaded || !isSignedIn || !userId) {
      return;
    }

    void identifyUser({
      id: userId,
      identityProvider: 'clerk',
    }).then(() => {
      // Prevent the old anonymous ID from replacing the Clerk ID after sign-out.
      document.cookie =
        'consent_visitor_id=; Max-Age=0; Path=/; SameSite=Lax';
    });
  }, [identifyUser, isLoaded, isSignedIn, userId]);

  return null;
}

identifyUser() replaces the subject's current externalId; it does not retain the anonymous ID as an alias. Once a visitor is linked to Clerk, stop supplying the former visitor ID. The example clears its client-readable cookie only after identifyUser() succeeds so a later render cannot change the subject back to the anonymous ID.

Info

Backend subject identification requires hosted mode (mode: 'hosted'). In offline mode, identifyUser() cannot update a backend subject. Identifiers supplied through options.user can still be held in client state and persisted with locally stored consent, so do not place sensitive values there.

Key Types

ConsentState

A record mapping consent category names to their boolean values:

type ConsentState = Record<AllConsentNames, boolean>;
// Example: { necessary: true, measurement: true, marketing: false }

ConsentInfo

Metadata about when and how consent was recorded:

interface ConsentInfo {
  time: number;                    // Epoch timestamp when consent was recorded
  subjectId?: string;              // Client-generated subject ID (sub_xxx format)
  externalId?: string;             // External user ID linked via identifyUser()
  identityProvider?: string;       // Identity provider (e.g. 'clerk', 'auth0')
}

LocationInfo

Detected geographic location from the c15t backend:

interface LocationInfo {
  countryCode: string;     // ISO 3166-1 alpha-2 (e.g. 'DE')
  regionCode: string;      // Region/state code (e.g. 'BY')
  jurisdiction: string;    // Applicable jurisdiction (e.g. 'GDPR', 'CCPA')
}

Model

The active consent model:

type Model = 'opt-in' | 'opt-out' | 'iab' | null;
  • 'opt-in' — Explicit consent required before tracking (GDPR)
  • 'opt-out' — Tracking allowed by default, user can opt out (CCPA)
  • 'iab' — IAB TCF 2.3 compliance mode
  • null — No jurisdiction detected yet