Skip to main content

Next.js

Upgrade from v2

Upgrade this Next.js app to c15t v3

Upgrade this Next.js app from c15t v2 to c15t v3.

Read https://v3.c15t.com/docs/frameworks/next/upgrade-v3.md first and follow it in order. Do not guess v3 APIs from memory.

  1. List every c15t dependency and import: @c15t/nextjs, @c15t/react, @c15t/scripts, @c15t/dev-tools and c15t. Note whether the app uses the App Router, the Pages Router or both.
  2. Check that React is 18 or later and Next.js is 15 or later. Stop and tell me if either needs a major upgrade.
  3. Replace the packages with c15t@alpha, plus @c15t/integrations@alpha if the app uses vendor helpers.
  4. Run npx @c15t/cli@alpha codemods packages-to-c15t consent-provider-options root-exports-to-subpaths use-consent-manager-to-hooks scripts-to-integrations dev-tools-to-c15t policy-packs-to-policy-rules callbacks-to-v3 theme-to-consent-theme iab-option-to-iab-provider css-variables-to-v3 postcss-tailwind3 --dry-run --json. Review the output, then run the same command without --dry-run. The codemods leave a TODO(c15t v3) comment wherever they need you to finish a change.
  5. If the app used fetchInitialData(), C15tPrefetch or ssrData, move it to ConsentRoot with resolveConsent(). Otherwise keep browser initialization with the ConsentProvider and hosted() the codemod wrote.
  6. Update callbacks, policy presets, renamed options and components, theme CSS, CSS variables and IAB setup as the guide describes.
  7. Keep the existing backend URL. If the app runs its own c15t backend, stop and tell me, because it has to be upgraded in the same release with https://v3.c15t.com/docs/self-host/upgrade-v3.md.
  8. Run the typecheck and build, and resolve every TODO(c15t v3) comment.

When you finish, list the files you changed, anything you could not migrate, and how you checked that vendor requests still wait for consent.

Before you start

  • Upgrade React and React DOM to 18 or later, and Next.js to 15 or 16.
  • If you run your own c15t backend, upgrade it in the same release. A v3 client can't read a v2 backend. See Upgrade a self-hosted backend. Inth-hosted backends need no change.

Replace the packages

Remove @c15t/nextjs, @c15t/react and @c15t/scripts, then install c15t from the alpha dist-tag. npm's default tag still resolves v2.

npm install c15t@alpha @c15t/integrations@alpha

Drop @c15t/integrations@alpha if you don't use its vendor helpers.

v2 importv3 import
@c15t/nextjsc15t/next. Server helpers are in c15t/next/server, c15t/next/pages and c15t/next/api.
@c15t/reactc15t/next, which re-exports the React components and hooks
@c15t/nextjs/styles.cssNothing: remove the import. Keep c15t/next/styles.css only with styles: false
DevTools from @c15t/dev-tools/reactDevTools from c15t/next/devtools
@c15t/scripts/*@c15t/integrations/*

The packages-to-c15t codemod rewrites the @c15t/nextjs and @c15t/react imports and removes the stylesheet imports. It doesn't edit package.json.

@c15t/nextjs is still published at v3, so its imports keep working if you pin it to @alpha. These docs use c15t/next.

Every v3 package ships ESM only. If a CommonJS file in your app calls require() on a c15t package, convert it to ESM.

Rename the integrations dependency

v3 renames @c15t/scripts to @c15t/integrations. Remove @c15t/scripts from your dependencies and install @c15t/integrations from the alpha dist-tag:

npm install @c15t/integrations@alpha

Replace the package name in each import. Vendor subpaths and helper names stay the same.

Before, in v2:

import { googleTagManager } from '@c15t/scripts/google-tag-manager';
import { posthog } from '@c15t/scripts/posthog';

After:

import { googleTagManager } from '@c15t/integrations/google-tag-manager';
import { posthog } from '@c15t/integrations/posthog';

The scripts configuration option and the Script type keep their names.

Two helper behaviors change. A helper that needs an ID, such as metaPixel, googleTagManager or posthog, now logs an error and skips its script when the ID is empty or only whitespace, where v2 loaded a broken script. And if your code reads @c15t/integrations/registry, an entry's consentCategory can now be a compound condition such as { and: ['marketing', 'measurement'] }, not only a category name.

@c15t/scripts stays available as a deprecated compatibility package for all of v3. It re-exports the implementation and types of @c15t/integrations, so you can migrate imports separately from the rest of the upgrade. Compatibility ends in v4; versions already published stay on npm.

To preview the import changes in JavaScript and TypeScript files:

npx @c15t/cli@alpha codemods scripts-to-integrations --dry-run --json

Review the output, then run it again without --dry-run. The codemod runs only when you name it. It does not change package.json, lockfiles, or imports inside .vue, .svelte or .astro files; update those by hand, then reinstall dependencies and build the app.

Replace useConsentManager()

useConsentManager() returned the whole consent state, so every component that called it re-rendered on every change. v3 has one hook per field. Run the codemod first:

npx @c15t/cli@alpha codemods use-consent-manager-to-hooks --dry-run --json

Review the proposed files, then run it again without --dry-run. For this v2 component:

Before (v2): components/accept-button.tsx
import { useConsentManager } from '@c15t/react';

export function AcceptButton() {
	const { activeUI, has, saveConsents } = useConsentManager();
	if (activeUI !== 'banner' || has('marketing')) return null;
	return (
		<button type="button" onClick={() => void saveConsents('all')}>
			Accept
		</button>
	);
}

the codemod writes:

After (v3): components/accept-button.tsx
import { useActiveUI, useConsent } from '@c15t/react';
import { useHeadlessConsentUI } from '@c15t/react/headless';

export function AcceptButton() {
	const activeUI = useActiveUI() ?? 'none';
	const hasMarketing = useConsent('marketing');
	const { saveCustomPreferences: saveConsents } = useHeadlessConsentUI();
	if (activeUI !== 'banner' || hasMarketing) return null;
	return (
		<button type="button" onClick={() => void saveConsents('all')}>
			Accept
		</button>
	);
}

The codemod keeps the entry you imported from. Change @c15t/react to c15t/react afterwards if you moved to the umbrella package. Fields it cannot rewrite stay on a useConsentManager() call under a TODO(c15t v3) comment, which fails the build until you replace them. The table lists the v2 fields components read. The other fields held provider configuration, such as legalLinks, scripts and storageConfig, or store internals; set configuration through ConsentProvider props. Import hooks from c15t/react, c15t/next or c15t/tanstack-start, and useHeadlessConsentUI from the matching /headless entry.

v2 fieldv3 replacement
activeUIuseActiveUI(). It returns null until c15t picks a surface, where v2 returned 'none'.
setActiveUI(ui)useSetActiveUI()
has(category)useConsent(category), one call per category at the top of the component
consentsuseConsents()
saveConsents('all')useHeadlessConsentUI().saveCustomPreferences('all')
saveConsents('necessary')useHeadlessConsentUI().saveCustomPreferences('none')
saveConsents('custom')useConsentDraft().save(), or useHeadlessConsentUI().saveCustomPreferences() inside a ConsentDraftProvider
selectedConsentsuseConsentDraft().values
setSelectedConsent(name, value)useConsentDraft().set(name, value)
setConsent(name, value)useSaveConsents(), called with { [name]: value }
consentInfouseExplicitChoice()
hasConsented()useExplicitChoice() !== null
consentCategories, consentTypes, getDisplayedConsents()useConsentDraft().displayedCategories, with labels from useTranslations().consentTypes
policyCategoriesusePolicyCategories(). The v2 list started with 'necessary'; this one does not.
policyScopeModeusePolicyScopeMode()
policyBannerusePromptPresentation()
policyDialogusePreferencesPresentation()
modeluseModel(). It returns null while no policy matches, where v2 returned 'opt-in'.
brandinguseBranding(). It returns null when unset, where v2 returned 'c15t'.
iabuseIABSnapshot()
locationInfouseLocation()
overrides, setOverrides()useOverrides(), useSetOverrides()
setLanguage()useSetLanguage()
user, identifyUser()useUser(), useIdentify()
translationConfiguseTranslations() for the active strings
subscribeToConsentChanges(listener)useSubscribeToConsentChanges(), or the onPermissionsChanged callback
updateConsentCategories(categories)useRegisterConsentCategories()
managerNothing. Use the hooks above.

saveCustomPreferences() saves the way the stock buttons do. It closes the banner or dialog after a successful save. useSaveConsents() calls the engine directly and does not close anything.

In v2, useConsentManager() kept one draft per component. In v3, useConsentDraft() and useHeadlessConsentUI() share a draft only inside a ConsentDraftProvider. Without one, saveCustomPreferences() saves a fresh draft and ignores what useConsentDraft().set() staged, even in the same component. Save with useConsentDraft().save(), or render the code that stages and the code that saves inside one ConsentDraftProvider. ConsentWidget already includes one. The codemod adds a TODO(c15t v3) comment where this applies.

Replace ConsentManagerProvider

A typical v2 App Router app wrapped the layout in a client component:

Before (v2): components/consent-manager/provider.tsx
'use client';

import {
	ConsentBanner,
	ConsentDialog,
	ConsentManagerProvider,
} from '@c15t/nextjs';

export default function ConsentManagerClient({ children }) {
	return (
		<ConsentManagerProvider
			options={{ mode: 'hosted', backendURL: 'https://your-project.c15t.dev' }}
		>
			<ConsentBanner />
			<ConsentDialog />
			{children}
		</ConsentManagerProvider>
	);
}

v3 gives you two ways forward.

Resolve consent on the server. This is the recommended path. The server reads the visitor's policy and stored choice, so gated scripts can start right after hydration and the banner can be in the first HTML. Replace the client component with ConsentRoot. Pass it resolveConsent() from c15t/next/server as state and your defineConsentConfig() result as config, then add the manifest route. ConsentRoot already wraps ConsentProvider, so don't mount both. If v2 set storageConfig.storageKey, pass the same name to resolveConsent() as cookieName, or the server won't find the stored choice. Follow the App Router guide or the Pages Router guide. This replaces v2's fetchInitialData(), ssrData and C15tPrefetch, which are gone.

Keep browser initialization, as v2 did without fetchInitialData(). Keep your client component, import from c15t/next, and change the provider:

After (v3): components/consent-manager/provider.tsx, changed lines
import { ConsentBanner, ConsentDialog, ConsentProvider } from 'c15t/next';
// c15t/next's hosted() is data for c15t.config.ts; the provider takes the transport.
import { hosted } from 'c15t/react';

// In the component, replace ConsentManagerProvider:
<ConsentProvider
  options={{ mode: hosted({ backendURL: 'https://your-project.c15t.dev' }) }}
>

The consent-provider-options codemod makes this change, including the hosted import:

npx @c15t/cli@alpha codemods consent-provider-options --dry-run --json

Static exports use browser initialization too. See static export and client-side setup.

Either way, remove the @c15t/nextjs/styles.css import, as stylesheets explains, and keep your backend URL. An existing hosted URL such as https://your-project.c15t.dev keeps working with v3 clients. New Inth projects use URLs such as https://your-project.inth.app.

The other v2 modes map to transports:

v2 optionv3 option
mode: 'offline' with offlinePolicymode: offline({ policyRules })
mode: 'custom' with endpointHandlersmode: custom(transport), where transport implements the v3 transport interface. endpointHandlers throws.

The codemod rewrites the offline mode too. It can't translate endpointHandlers, so it leaves a TODO(c15t v3) comment on a custom mode.

Offline mode keeps choices in the browser and records nothing on a server. Not recommended for production environments.

Replace callbacks

Callbacks stay under callbacks in your options, with new names:

v2 callbackv3 callback
onConsentSetonPermissionsChanged to react to what may run now. onChoiceRecorded to react to the visitor's accept, reject or save.
onConsentChangedonChoiceRecorded. See the note below the table.
onBannerFetchedNo callback. In React and Next.js, read usePolicyResolution() to know when the policy has resolved. In JavaScript, read resolution from the kernel snapshot.
onError, onBeforeConsentRevocationReloadUnchanged

The callbacks-to-v3 codemod renames onConsentChanged to onChoiceRecorded. It leaves a TODO(c15t v3) comment there about the new payload, and on onConsentSet and onBannerFetched, which it can't choose a replacement for:

npx @c15t/cli@alpha codemods callbacks-to-v3 --dry-run --json

v2's onConsentChanged fired only when a save changed at least one category's value, and passed preferences and previousPreferences. v3's onChoiceRecorded fires for every accept, reject or save that confirms a category, including one that saves the same values again, because each save renews the choice's confirmation time. Its payload carries snapshot, confirmed and actionAt. v3 has no callback that fires only when a save changes a value. If you need the before and after values, use onPermissionsChanged: it passes previous and the new snapshot whenever effective permissions change, whether a save or something else changed them.

v2's onConsentSet also fired on initialization and hydration. In v3, onChoiceRecorded runs only when the visitor acts, and onPermissionsChanged runs when effective permissions change for any reason, including expiry, a policy update or a privacy signal. Neither runs at startup if the resolved permissions match the defaults, such as an opt-in policy that leaves every category denied. If an external API needs the starting state, read it once at startup as well: in React and Next.js from useEffectivePermissions() in an effect, which also runs on later changes, and in JavaScript from kernel.events.on('init:applied', ({ snapshot }) => ...).

After (v3): callbacks in your options
callbacks: {
  onChoiceRecorded(event) {
    console.log('Visitor recorded a choice', event);
  },
  onPermissionsChanged(event) {
    console.log('Effective permissions changed', event);
  },
},

Move policy packs to policy rules

v2 policy packs become policy rules, and policyPackPresets becomes policyRulePresets, exported from c15t. Where the rules live depends on your backend:

  • Inth: set the rules in your Inth project. Rules in client code do not change a hosted policy.
  • Self-hosted: move the backend's policyPacks to manifest.policyRules. See policy configuration.
  • Offline: move offlinePolicy.policyPacks to offline({ policyRules }).
After (v3): offline policy rules
import { offline, policyRulePresets } from 'c15t';

const mode = offline({
	policyRules: [
		policyRulePresets.europeOptIn(),
		policyRulePresets.worldOptOutNoPrompt(),
	],
});

In Next.js, import offline from c15t/next instead and set the result as mode in c15t.config.ts. Both take the same policyRules.

v2 presetv3 preset
europeOptIn(), europeIab()Same names
californiaOptIn(), californiaOptOut()Same names
quebecOptIn()Same name
worldNoBanner()worldOptOutNoPrompt()

worldNone() is a separate, new preset: it shows nothing and grants no rights, while worldOptOutNoPrompt() keeps v2's opt-out and preference rights.

The policy-packs-to-policy-rules codemod renames policyPackPresets and worldNoBanner(), and moves the import to c15t where it came from c15t/react or c15t/next. consent-provider-options moves offlinePolicy.policyPacks into offline({ policyRules }). Preset calls need no other change, but a hand-written v2 pack uses consent and ui keys, which v3 rules replace with flat keys such as model and prompt; the codemods mark those with a TODO(c15t v3) comment.

v3 adds presets for more regions. offline() without policyRules uses recommendedPolicyRules(). Read policies before you change a rule's model or prompt, and how consent works for the difference between a permission and a recorded choice.

Rename options and components

v2v3
translationsi18n
backendURL, mode: 'hosted'mode: hosted({ backendURL }) from c15t/react on ConsentProvider. Next.js ConsentRoot: NEXT_PUBLIC_C15T_BACKEND_URL and c15t.config.ts.
ssrData (Next.js)ConsentRoot with state from resolveConsent()
iframeBlockerConfigiframeBlocker, or false to turn it off
Frame, FrameRoot, FrameTitle, FrameButton, FramePropsConsentGate and its parts. The Frame names still work as deprecated aliases and log a one-time warning outside production.
frame translations, such as frame.titleconsentGate, such as consentGate.title. Copy under frame still applies and logs a warning outside production.
FrameTranslationsConsentGateTranslations. FrameTranslations still works as a deprecated alias.
--frame-* CSS variables--consent-gate-*, with the same suffixes, such as --consent-gate-placeholder-background-color. The old names no longer apply.
YouTubeEmbed, GoogleMapRemoved. Wrap your own embed in ConsentGate.
useConsentScript()Removed. Register the script in scripts with a @c15t/integrations helper.
useSSRStatus()Removed
ConsentButtonRemoved. Use the ConsentBanner and ConsentWidget buttons, or call useHeadlessConsentUI() from your own button.
@c15t/react/cookie-bannerc15t/react/consent-banner
retryConfigRemoved
store, including namespace, debug, initialConsentCategories and initialTranslationConfigRemoved. Use the top-level consentCategories and i18n options. Debug information is on window.c15t.
window.c15tStorewindow.c15t
iab: iab({ ... })IABProvider. See Move IAB setup.

The consent-provider-options codemod renames ConsentManagerProvider and its option and prop types, turns mode and backendURL into hosted() or offline(), and renames iframeBlockerConfig. v2 still accepted the deprecated translations option; the translations-to-i18n codemod moves it to i18n.

npx @c15t/cli@alpha codemods consent-provider-options translations-to-i18n --dry-run --json

ConsentBanner, ConsentDialog, ConsentDialogLink, ConsentDialogTrigger and ConsentWidget keep their names. The scripts, networkBlocker, legalLinks, overrides, theme and colorScheme options keep theirs too, but theme tokens no longer produce CSS on their own. See Update styles and themes.

persistence and storageConfig are read once, when the provider mounts. v2 moved a stored choice when the storage key changed. Remount the provider to change storage. Reading a vendor that nothing declares, through vendors, a script or the backend, returns false.

Some names left the root c15t/react entry:

v2 import from the rootv3 import
useHeadlessConsentUI, useConsentDialogTrigger, useColorScheme, useFocusTrap and the translation helpersc15t/react/headless
Flat banner parts such as ConsentBannerCard and ConsentBannerAcceptButtonConsentBanner.Card, ConsentBanner.AcceptButton and the other compound parts, or the same names from c15t/react/components/consent-banner
TriggerRoot, TriggerButton, TriggerIcon, TriggerText, useTriggerContext, useDraggablec15t/react/consent-dialog-trigger
Token types such as ColorTokens and TypographyTokensc15t/react/types. Theme stays on the root.
configureConsentManager, policyPackPresets, ConsentStoreState, ConsentManagerInterfaceRemoved. Use policyRulePresets from c15t.

The root-exports-to-subpaths codemod moves these imports to their new entries and marks removed names with a TODO(c15t v3) comment:

npx @c15t/cli@alpha codemods root-exports-to-subpaths --dry-run --json

ConsentGate changes one behavior from Frame. When its category is granted, the embed now mounts after hydration instead of in the server HTML, so an iframe loads once instead of twice. Size the wrapper with className or style to hold its space. Test ids change from frame-placeholder and frame-open-dialog to consent-gate-placeholder and consent-gate-button.

Update styles and themes

Render theme CSS

In v2 the provider built CSS from options.theme in the browser. In v3 it doesn't. Without a change, custom colors, fonts and radii fall back to the defaults, and you only see a development warning.

Render ConsentTheme with the same theme object, or put the output of generateThemeCSS(theme) in your own stylesheet. Keep options.theme for consentActions and slots. See customization and tokens.

The theme-to-consent-theme codemod marks a provider theme that sets tokens with a TODO(c15t v3) comment.

A generated theme uses :root:root, so your own --c15t-* values on plain :root lose to it. Put them in the theme instead, or raise your selector.

Stylesheets

  • Remove the c15t stylesheet import, such as @c15t/react/styles.css or @c15t/nextjs/styles.css. The stock components in c15t/react, c15t/next and c15t/tanstack-start render the rules they use as <style> elements, so no stylesheet request holds back the first paint. An import you keep still styles the components, but the rules ship twice and the stylesheet still delays the first paint.
  • Keep an import of the v3 stylesheet, such as c15t/react/styles.css, only with Tailwind CSS 3 or a named cascade layer, and set styles: false in the provider options so the components add no second copy. See stylesheets and CSS layers. The packages-to-c15t codemod removes @c15t/react and @c15t/nextjs stylesheet imports. With Tailwind CSS 3 or a cascade layer it keeps them, pointed at c15t, with a TODO(c15t v3) comment.
  • With manual styles, iab/styles.css no longer repeats the default tokens. Import it after styles.css. Automatic IAB styling needs neither import.
  • Tailwind CSS 3 needs a PostCSS plugin for c15t's styles.css. Add 'c15t/postcss-tailwind3': {} before tailwindcss in postcss.config, and set styles: false. See Tailwind CSS 3. Create React App can't run the plugin. The postcss-tailwind3 codemod adds it to an object-form postcss.config and warns about configs it can't edit.

CSS variables and slots

v2v3
--consent-widget-*--consent-manager-*, used by ConsentDialog and ConsentWidget
--consent-widget-accordion-*Removed. Set --accordion-* instead.
--frame-*--consent-gate-*
--consent-banner-entry-animation, --consent-banner-exit-animationRemoved
theme.slots.frameconsentGate, consentGateTitle and consentGateButton
theme.slots.consentDialogFooterconsentWidgetFooter

The css-variables-to-v3 codemod renames --consent-widget-* and --frame-* in stylesheets and inline styles, and marks --consent-widget-accordion-* with a comment instead of renaming it, because --accordion-* styles every accordion. theme-to-consent-theme renames theme.slots.consentDialogFooter and marks theme.slots.frame.

npx @c15t/cli@alpha codemods theme-to-consent-theme css-variables-to-v3 postcss-tailwind3 --dry-run --json

theme.slots now applies in React. v2 accepted it in types and ignored it, so check slots you set before the upgrade.

In the App Router, render ConsentTheme from a Server Component such as the root layout. In the Pages Router, render it in pages/_document.tsx.

Move IAB setup to IABProvider

React, Next.js and TanStack Start no longer take an iab provider option. Render IABProvider from c15t/react/iab inside ConsentProvider or ConsentRoot, around the IAB components. Its props are the options you passed to iab(), such as cmpId, vendors, customVendors and the new publisherRestrictions.

The iab-option-to-iab-provider codemod marks the iab option with a TODO(c15t v3) comment; move it by hand.

A cmpId that isn't an integer from 2 to 4095 now throws. Without a cmpId, the CMP doesn't mount, so there is no __tcfapi and no TC string.

@c15t/iab no longer exports createIABManager, decodeTCString, generateTCString or c15tToIabPurposes. Use createIAB({ kernel, cmpId }), which returns a handle with generateTCString(), acceptAll(), rejectAll() and save(). If your app depends on @c15t/iab directly, upgrade it to @c15t/iab@alpha too, or your import resolves to the v2 package. Remove it if IABProvider now covers everything you used it for.

Server integrations now send a reference to the Global Vendor List instead of the list, and the browser fetches it when the IAB module mounts. If you set a Content Security Policy, allow the list's origin in connect-src, or 'self' for same-origin routes.

See the IAB guide for a complete setup.

Update headless UI code

useHeadlessConsentUI() keeps its name and moves to c15t/react/headless. The root-exports-to-subpaths codemod moves the import. Some of its methods take different arguments:

v2v3
performAction(action, { surface })performAction(action). It also accepts 'dismiss' and 'customize'.
openBanner({ force })openBanner()
saveCustomPreferences({ uiSource })saveCustomPreferences(), saveCustomPreferences('all') or saveCustomPreferences('none')
banner.layout, dialog.layout, hasPolicyHintsbanner and dialog carry variant, position, rights, requiredActions and diagnostics

The UseHeadlessConsentUIResult type is no longer exported. Use ReturnType<typeof useHeadlessConsentUI> if you need it.

Update complete translation objects

Partial overrides through i18n.messages and objects typed as Translations need no change. A complete translation object typed as CompleteTranslations fails type-checking until you add the new sections and keys: consentGate, which replaces frame, rights, the notice and dismiss strings such as common.acknowledge and cookieBanner.noticeTitle, and the vendor list copy under consentManagerDialog.vendors. migrateLegacyTranslationKeys() moves frame copy to consentGate.

i18n.messages for the visitor's language now merge over the backend's copy instead of being replaced by it.

Keep visitors' existing choices

v3 uses the same c15t cookie and storage key as v2 and reads v2 records. It does not rewrite them on page load; the next accept, reject or save writes a v3 record.

A v2 denial keeps restricting its category. A v2 grant keeps applying until the policy's validity period runs out, and then c15t asks again. If the rule comes from a preset and the v2 record noted the policy it was made under, c15t also asks again when that policy has changed. Under custom rules, a v2 grant applies until it expires. Do not copy permissions into new records yourself; c15t decides which v2 choices still count.

Update dev tools

@c15t/dev-tools/react and @c15t/dev-tools/tanstack are gone. Import DevTools, C15tTanStackDevtoolsPanel and c15tDevtools() from c15t/next/devtools. Dev tools read the consent engine, so namespace, createStoreConnector() and getC15tStore() are gone too. See dev tools.

The dev-tools-to-c15t codemod points the imports at c15t/next/devtools and removes the namespace prop:

npx @c15t/cli@alpha codemods dev-tools-to-c15t --dry-run --json

Upgrade from an earlier v3 alpha

The v3 alphas renamed and removed APIs that never shipped in a stable release. Names that were in stable v2 keep a deprecated alias. Every other renamed name is removed with no alias, so a build that still uses one fails to type-check or throws at runtime.

Deprecated, still working

Only React's v2 Frame names remain. They render ConsentGate and, outside production, log a one-time warning such as c15t: `Frame` is deprecated and will be removed in a future major release. Use `ConsentGate` instead.

PackageDeprecatedUse instead
c15t/react, c15t/next, c15t/tanstack-startFrame, FrameRoot, FrameTitle, FrameButton, and the FrameProps typeConsentGate and its parts, and ConsentGateProps

v2's ConsentManagerProvider and string modes such as mode: 'hosted' are handled by the consent-provider-options codemod, not by aliases.

Removed

Modes and build integrations, in every framework:

RemovedUse instead
hosted({ url })hosted({ backendURL }), or the framework's backend URL variable
createManifestTransport({ manifest })createManifestTransport({ snapshot })
assertDecisionInputs defaulting to false on hosted()It now defaults to true whenever initURL is set. Pass false to turn it off.
hostedModes from c15t/runtime/providerreadHostedMode(mode)
buildManifest: true in Astro and NuxtonBuildError: 'fail', the default for production builds. buildManifest: false is manifest({ source: 'runtime' }).
A generated c15t-manifest.ts and its .gitignore entryimport { snapshot } from 'c15t/generated'. Most apps no longer import it at all.
The build options outputFile, exportName, importSource and rootDirRemoved. The snapshot is a virtual module, or a file under node_modules/.cache/c15t/ in Next.js.

Next.js:

RemovedUse instead
defineConsentConfig({ manifestURL, initURL })routePrefix, or mode: manifest({ resolve: 'browser', manifestURL })
A named consentConfig export, passed to ConsentRoot as configexport default defineConsentConfig(...) in c15t.config.ts. withConsentManifest finds the file. config stays as an optional override.
ConsentRoot's backendURL propbackendURL in the config, or NEXT_PUBLIC_C15T_BACKEND_URL
The 'use client' wrapper that passed config and scriptsscripts and other browser options in c15t.config.ts. A Server Component renders ConsentRoot directly.
createNextConsentRouteHandlers(), its GET and manifestGETcreateConsentRoute() in one catch-all route, app/api/c15t/[...c15t]/route.ts
createPagesApiHandlers() and its init and manifestcreatePagesConsentRoute() in pages/api/c15t/[...c15t].ts
resolveConsent(...) with a JSON round trip in getServerSidePropsexport const getServerSideProps = withConsentProps(), and AppProps<ConsentPageProps> in _app.tsx
resolveConsent({ manifest }), and manifest on the route helperssnapshot, which defaults to the build's
ConsentManifestOptions and the c15t.server.ts moduleResolveConsentOptions or NextConsentRouteOptions. The helpers read the config and snapshot themselves.
resolveStrictestDefaultInit from c15t/next/staticresolveUnknownLocationInit
ConsentRoot falling back to offline mode when no config or NEXT_PUBLIC_C15T_BACKEND_URL gives it a backend URLConsentRoot throws. Set mode: offline() to run without a backend.
onBuildError: 'runtime' to build a hosted() or offline() app while the backend is unreachableNothing. withConsentManifest reads the mode from c15t.config.ts and skips the download for modes that don't use it.
hosted, offline and manifest from c15t/next as transportsThey are data for c15t.config.ts now. A ConsentProvider imports them from c15t/react.

Update the CLI

  • Run c15t login again. The CLI signs in through Inth now and no longer reads sessions saved by v2.
  • c15t setup no longer writes .env.local or .env.example and no longer accepts --env. It writes the backend URL into the files it generates.
  • c15t setup no longer detects Remix or Gatsby. Pick a guide from Choose your setup for those apps.

Check the migration

  1. Run your typecheck and build. Remaining TODO(c15t v3) comments from the codemod fail the build until you resolve them.
  2. Load the app with a v2 consent cookie from before the upgrade. The banner stays closed if that choice still applies, and a rejected category stays blocked.
  3. In a private window, check in DevTools Network that vendor requests wait for consent, start after you accept, and stay absent after you reject and reload.
  4. Reopen preferences from your privacy settings link and change a category.

The verification guide has the full release checklist.