Skip to main content

JavaScript Modules

Persistence

Where choices are stored

The persistence module writes each recorded choice to a first-party cookie and to localStorage, and reads them back when the page loads. The visitor's choice survives reloads and later visits without a backend request. @c15t/browser and createConsentRuntime run it by default.

Stored itemName, with the default key
Category choicec15t cookie and localStorage key
Notice acknowledgementc15t-notice
Vendor choicec15t-vendors
Saves waiting to reach the backenda separate localStorage queue

The cookie is readable by your server, so a server-rendered page can see the visitor's choice before any script runs.

When records are read and written

Stored records are read synchronously when the module starts, so a returning visitor's choice applies before the first banner renders.

The code that writes and reconciles records is a separate chunk. Once a banner or dialog has been shown, it starts loading in browser idle time after the page's load event. It also starts at the first write or reconciliation if one comes sooner. A returning visitor who sees no prompt downloads it only when something needs it. Until it has loaded:

  • A save waits for its record to be stored before its backend request leaves, and before the save resolves. On the first save of a page load, this can add the time it takes to fetch the chunk.
  • If the chunk fails to load, the save keeps waiting rather than count as stored. Its request does not leave, and a revocation reload does not run while storage still holds the old choice. c15t tries the chunk again after 1, 4 and 16 seconds, and at every later save, focus or tab change.
  • reconcile() returns false and runs when the chunk lands.

clear() needs no chunk. It removes every record from the cookie and localStorage, stores the clear epoch and resets the kernel's records before it returns, so reloading right after a clear cannot restore a cleared choice.

  • pagehide cannot store anything. A page restored from the back/forward cache stores its pending records when the chunk lands. A page that unloads first loses them, along with the save request still waiting for them.

After the chunk has loaded, each write runs in the macrotask after the visitor acts, and pagehide runs any write still waiting.

Name, domain and lifetime

storageConfig sets where and how long records live:

OptionDefaultWhat it does
storageKey'c15t'The cookie and localStorage name. Other records use it as a prefix.
crossSubdomainfalseSet the cookie on the root domain, so www.example.com and shop.example.com share it.
defaultDomaincurrent hostAn explicit cookie domain, such as '.example.com'. Wins over crossSubdomain.
defaultExpiryDays365Cookie lifetime in days. The policy's own validity still decides when a choice expires.

Use the same storageConfig on every site that shares the cookie. Pass it to init() or createConsentRuntime, or inside persistence for a runtime:

storageConfig: { crossSubdomain: true },

Keep open tabs in step

Tabs on the same origin share localStorage, so a choice made in one tab reaches the others at once through the storage event. A tab on another subdomain that shares only the cookie gets no such event. It reads stored records again when the page becomes visible or the window regains focus.

Call runtime.reconcileStorage() to read them at another moment, for example after a response set the consent cookie. With @c15t/browser, call client.runtime.reconcileStorage(). persistence: { sync: false } on the runtime or the @c15t/browser client turns the automatic reads off. Keep open tabs in step explains the merge rules.

Turn it off

persistence: false on createConsentRuntime or on init() from @c15t/browser keeps choices in memory only. Every page load starts without a choice, so the banner shows on every page.

Attach it to your own kernel

A kernel from createConsentKernel stores nothing. Create the module before commands.init() so stored choices apply first:

src/consent.ts
import { createConsentKernel, createHostedTransport } from 'c15t';
import { createPersistence } from 'c15t/modules/persistence';
import { createScriptLoader } from 'c15t/modules/script-loader';

import { scripts } from './scripts';

export const startConsentKernel = async function startConsentKernel(
	backendURL: string
) {
	const kernel = createConsentKernel({
		transport: createHostedTransport({ backendURL }),
	});
	// Each module is yours to create and dispose. This kernel has no iframe
	// blocker, network blocker, data clearing or reload after revocation.
	const persistence = createPersistence({ kernel });
	const loader = createScriptLoader({ kernel, scripts });
	await kernel.commands.init();
	return {
		dispose() {
			loader.dispose();
			persistence.dispose();
			kernel.dispose();
		},
		kernel,
	};
};

createPersistence({ kernel, storageConfig?, skipHydration?, sync? }) returns:

MemberWhat it does
hydrate()Read storage again. Returns whether any record was found.
reconcile()Merge records another tab or runtime stored. Returns whether anything changed.
clear()Delete every c15t record and reset the kernel's records.
dispose()Stop writing and remove the tab listeners. Stored records stay.

skipHydration: true skips the first read, for a kernel a server already seeded with the visitor's records.

Check it works

  1. Accept all and open DevTools, Application. A c15t cookie and a c15t localStorage entry exist.
  2. Reload. The banner stays closed.
  3. Open a second tab, reject all there and switch back. The first tab now shows the rejection.