---
title: Upgrade from v2
description: Upgrade a JavaScript app from the c15t v2 store and
  getOrCreateConsentRuntime to v3. Covers @c15t/browser, createConsentRuntime,
  the consent kernel, moved exports, callbacks, policies and stored consent.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
**Upgrade this JavaScript app to c15t v3**

Paste into Claude Code, Codex, Cursor or another coding agent at the root of your app.

```prompt
Upgrade this JavaScript app from the c15t v2 store to c15t v3.

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

1. Find every use of the `c15t` package and `@c15t/scripts`: `getOrCreateConsentRuntime`, `configureConsentManager`, `consentStore`, cookie helpers, script loader and iframe blocker functions.
2. Decide which v3 entry fits. Use `@c15t/browser` if the app wants the stock banner, `createConsentRuntime` from `c15t/runtime` if it renders its own UI, and `createConsentKernel` only if it mounts each module itself. Tell me which one you picked and why.
3. Install `c15t@alpha`, or `@c15t/browser@alpha` for the stock banner, plus `@c15t/integrations@alpha` if the app uses vendor helpers and `@c15t/dev-tools@alpha` if it uses dev tools.
4. Run `npx @c15t/cli@alpha codemods scripts-to-integrations consent-provider-options policy-packs-to-policy-rules callbacks-to-v3 --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 you picked `createConsentRuntime`, call `runtime.start()` after creating it. Replace each store call with its v3 equivalent from the guide's mapping table.
6. Update callbacks and policy presets 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](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

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](/docs/self-host/upgrade-v3).
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](/docs/frameworks/javascript/quickstart).|
|Your own UI, with persistence, the script loader and the blockers wired for you|`createConsentRuntime()` from `c15t/runtime`, then `runtime.start()`. See [headless](/docs/frameworks/javascript/headless).|
|The engine alone, mounting each module yourself|`createConsentKernel()` from `c15t`. See the [kernel API](/docs/frameworks/javascript/api/kernel).|

Install `c15t` for the runtime or the kernel:

|Package manager|Command|
|:--|:--|
|npm|`npm install c15t@alpha`|
|pnpm|`pnpm add c15t@alpha`|
|yarn|`yarn add c15t@alpha`|
|bun|`bun add c15t@alpha`|

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](/docs/frameworks/javascript/api/runtime) 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](/docs/frameworks/javascript/api/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:

|Package manager|Command|
|:--|:--|
|npm|`npm install @c15t/integrations@alpha`|
|pnpm|`pnpm add @c15t/integrations@alpha`|
|yarn|`yarn add @c15t/integrations@alpha`|
|bun|`bun add @c15t/integrations@alpha`|

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

Before, in v2:

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

After:

```ts
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:

```bash
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 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:

```bash
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 }) => ...)`.

```ts title="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](/docs/self-host/guides/policy-packs).
* **Offline**: move `offlinePolicy.policyPacks` to `offline({ policyRules })`.

```tsx title="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 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](/docs/concepts/policies) before you
change a rule's model or prompt, and
[how consent works](/docs/concepts/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](/docs/frameworks/javascript/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 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](/docs/concepts/choose-your-setup) for those apps.

## Check the migration

1. Run your typecheck and build.
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 and change a category.

The [verification guide](/docs/guides/verify-consent) has the full release
checklist.
