JavaScript
Upgrade from v2
Before you start
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.
Every v3 package ships ESM only. If a CommonJS file in your app calls
require() on a c15t package, convert it to ESM.
Pick a replacement
v2's getOrCreateConsentRuntime(), configureConsentManager() and the
Zustand consentStore are gone. Pick a replacement by how much you want c15t
to do.
| You want | Use |
|---|---|
| The stock banner and dialog | @c15t/browser. Call init() from a bundled app, or add the script tag to a site without a build step. See the quickstart. |
| Your own UI, with persistence, the script loader and the blockers wired for you | createConsentRuntime() from c15t/runtime, then runtime.start(). See headless. |
| The engine alone, mounting each module yourself | createConsentKernel() from c15t. See the kernel API. |
Install c15t for the runtime or the kernel:
The runtime takes the same kind of options v2 did. mode and backendURL
become one transport, such as hosted({ backendURL }) or offline({ policyRules }),
exported from c15t. translations becomes i18n, and
iframeBlockerConfig becomes iframeBlocker. See the
runtime reference for every option.
createConsentRuntime() only builds the runtime. Call runtime.start() to
read stored consent, resolve the policy and mount the script loader and
blockers. Until then nothing is gated.
The consent-provider-options codemod rewrites mode, backendURL and
iframeBlockerConfig in the object you pass to getOrCreateConsentRuntime()
and adds the hosted or offline import from c15t. Rename the call to
createConsentRuntime() from c15t/runtime yourself.
Replace store calls
The runtime exposes the kernel as runtime.kernel. Read state from
kernel.getSnapshot() and listen with kernel.subscribe().
| v2 store | v3 |
|---|---|
consentStore.getState() | kernel.getSnapshot() |
consentStore.subscribe(listener) | kernel.subscribe(listener). The listener gets the whole snapshot, on every change. |
subscribeToConsentChanges(listener) | kernel.events.on('permissions:changed', ({ snapshot }) => listener(snapshot.effectivePermissions)), or the onPermissionsChanged callback |
has(category) | kernel.getSnapshot().effectivePermissions[category] |
consents | kernel.getSnapshot().effectivePermissions |
consentInfo | kernel.getSnapshot().explicitChoice, which is null until the visitor chooses |
hasConsented() | kernel.getSnapshot().explicitChoice !== null |
saveConsents('all') | kernel.commands.save('all') |
saveConsents('necessary') | kernel.commands.save('none') |
selectedConsents, setSelectedConsent(name, value) | createPreferenceDraft(kernel) from c15t/preference-draft, then draft.getState() and draft.set(name, value) |
saveConsents('custom') | draft.save() on that preference draft |
setConsent(name, value) | kernel.commands.save({ [name]: value }) |
activeUI, setActiveUI(ui) | kernel.getSnapshot().activeUI, kernel.set.activeUI(ui) |
locationInfo | kernel.getSnapshot().location |
setOverrides(overrides) | runtime.setOverrides(overrides), then runtime.reinit() |
setLanguage(code) | runtime.setLanguage(code) |
identifyUser(user) | runtime.identify(user) |
effectivePermissions is what may run now. explicitChoice is what the
visitor decided. Under an opt-out policy a category can be allowed before any
choice, so don't treat a true permission as a recorded grant. See
snapshot.
Update moved exports
v2 export from c15t | v3 |
|---|---|
getCookie, setCookie, deleteCookie, getConsentFromStorage, saveConsentToStorage, deleteConsentFromStorage | Removed. createPersistence() from c15t/modules/persistence owns storage. |
loadScripts, unloadScripts, updateScripts, isScriptLoaded, getLoadedScriptIds | Removed. Pass scripts to createConsentRuntime(), which mounts the loader on start(). To run a loader yourself, createScriptLoader({ kernel, scripts }) from c15t/modules/script-loader returns updateScripts(next), getLoadedScriptIds() and dispose(). Use getLoadedScriptIds().includes(id) for isScriptLoaded(id). |
createIframeBlocker | c15t/modules/iframe-blocker |
getEffectivePolicy, filterConsentCategoriesByPolicy and the other policy helpers | resolveConsentPresentation() from c15t and the helpers in c15t/surface-actions |
C15tClient, OfflineClient, CustomClient, API_ENDPOINTS | Transports: hosted(), offline() and custom() from c15t |
policyPackPresets | policyRulePresets |
configureConsentManager, createConsentManagerStore, clearConsentRuntimeCache | Removed |
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:
Replace the package name in each import. Vendor subpaths and helper names stay the same.
Before, in v2:
After:
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:
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 callbacks
Callbacks stay under callbacks in your options, with new names:
| v2 callback | v3 callback |
|---|---|
onConsentSet | onPermissionsChanged to react to what may run now. onChoiceRecorded to react to the visitor's accept, reject or save. |
onConsentChanged | onChoiceRecorded. See the note below the table. |
onBannerFetched | No callback. In React and Next.js, read usePolicyResolution() to know when the policy has resolved. In JavaScript, read resolution from the kernel snapshot. |
onError, onBeforeConsentRevocationReload | Unchanged |
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:
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 }) => ...).
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
policyPackstomanifest.policyRules. See policy configuration. - Offline: move
offlinePolicy.policyPackstooffline({ policyRules }).
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 preset | v3 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.
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
Dev tools no longer depend on React. Upgrade @c15t/dev-tools to
@c15t/dev-tools@alpha with the rest of c15t, then call
createDevTools({ kernel }) with your runtime's kernel. See
dev tools.
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.
Removed
Modes and build integrations:
| Removed | Use 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/provider | readHostedMode(mode) |
buildManifest: true in Astro and Nuxt | onBuildError: 'fail', the default for production builds. buildManifest: false is manifest({ source: 'runtime' }). |
A generated c15t-manifest.ts and its .gitignore entry | import { snapshot } from 'c15t/generated'. Most apps no longer import it at all. |
The build options outputFile, exportName, importSource and rootDir | Removed. The snapshot is a virtual module, or a file under node_modules/.cache/c15t/ in Next.js. |
@c15t/browser:
| Removed | Use instead |
|---|---|
init({ backendURL }) | init({ mode: hosted() }), or init({ mode: manifest() }) with consentManifest() from c15t/build |
init({ mode: 'hosted' }), 'manifest', 'offline' | init({ mode: hosted() }), manifest(), offline(). Names work only in the script-tag builds. |
init({ manifest, manifestURL }) | init({ mode: manifest({ snapshot, manifestURL }) }) |
init({ policyRules: ['europeOptIn'] }) | init({ mode: offline({ policyRules: [policyRulePresets.europeOptIn()] }) }) |
manifest({ manifest }) | manifest({ snapshot }) |
The script-tag builds and the @c15t/browser/hosted and
@c15t/browser/offline entries keep backendURL, manifest, manifestURL,
policyRules and mode names.
Update the CLI
- Run
c15t loginagain. The CLI signs in through Inth now and no longer reads sessions saved by v2. c15t setupno longer writes.env.localor.env.exampleand no longer accepts--env. It writes the backend URL into the files it generates.c15t setupno longer detects Remix or Gatsby. Pick a guide from Choose your setup for those apps.
Check the migration
- Run your typecheck and build.
- 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.
- 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.
- Reopen preferences and change a category.
The verification guide has the full release checklist.