@consentify/core
Headless consent state management with cookie storage, policy versioning, typed events, multi-tab sync, and Google Consent Mode v2.
@consentify/core is the headless foundation of the Consentify SDK. It handles consent storage (cookie and/or localStorage), policy versioning via content hashing, typed events, multi-tab sync via BroadcastChannel, and optional Google Consent Mode v2 wiring.
Zero dependencies, SSR-safe.
Installation
npm install @consentify/coreCreating an Instance
import { createConsentify } from '@consentify/core';
export const consentify = createConsentify({
policy: {
categories: ['analytics', 'marketing', 'preferences'] as const,
},
});Options
createConsentify({
policy: {
categories: ['analytics', 'marketing'] as const, // required
identifier: 'v2', // optional explicit policy version
},
mode: 'opt-in', // 'opt-in' (GDPR, default) | 'opt-out' (CCPA)
storage: ['cookie', 'localStorage'], // storage order; default: ['cookie']
consentMaxAgeDays: 180, // expire consent after N days (default: never)
expirationWarningDays: 30, // emit 'expiring' event N days before expiry
cookie: {
name: 'consentify', // default: 'consentify'
maxAgeSec: 31_536_000, // default: 1 year
sameSite: 'Lax', // 'Lax' | 'Strict' | 'None'
secure: true, // default: true (forced to true for 'None')
path: '/',
domain: undefined,
},
});Cloud Mode
Passing a siteId instead of a policy turns on cloud mode: the factory
becomes async, fetches the category list from the server, and reports consent
events back for your analytics dashboard.
const consentify = await createConsentify({
siteId: 'your-site-id',
endpoints: {
config: 'https://consentify.dev/api/sdk',
ingest: 'https://consentify.dev/api/sdk',
},
});The package's default endpoints (cdn.consentify.dev /
ingest.consentify.dev) are not currently served. Always set endpoints
explicitly as shown - see SDK endpoints.
Consent State
All read methods return a discriminated union:
type ConsentState<T extends string> =
| { decision: 'unset' }
| {
decision: 'decided';
snapshot: {
choices: Record<'necessary' | T, boolean>;
givenAt: string; // ISO timestamp
policy: string; // policy hash
};
};Client API (flat, top-level)
The instance exposes a flat API for browser use:
get()
const state = consentify.get();
if (state.decision === 'unset') {
// no consent given yet - show banner
}
if (state.decision === 'decided') {
console.log(state.snapshot.choices);
}set(choices)
consentify.set({ analytics: true, marketing: false });acceptAll() / rejectAll()
consentify.acceptAll();
consentify.rejectAll();clear()
Remove consent. get() then returns { decision: 'unset' }.
consentify.clear();isGranted(category)
if (consentify.isGranted('analytics')) {
loadAnalytics();
}subscribe(callback)
Low-level subscription; fires on any consent change (set, clear, cross-tab sync).
const unsubscribe = consentify.subscribe(() => {
const state = consentify.get();
console.log('consent changed:', state);
});guard(category, onGrant, onRevoke?)
One-shot grant handler with optional revoke cleanup. Returns a disposer.
consentify.guard('analytics',
() => loadAnalyticsScript(),
() => unloadAnalyticsScript(),
);getProof()
Returns a signed consent snapshot for audit logs. null if decision === 'unset'.
const proof = consentify.getProof();
// { policy, givenAt, choices, signature }By default the signature is a non-cryptographic FNV-1a hash. This variant is
deprecated - the signature is forgeable, and a one-time runtime warning is
emitted the first time getProof() is called without a secret.
For real audit proofs, pass a secret to createConsentify() (server-only -
never in client code, where it would be visible to end users). With a secret
set, getProof() becomes async and returns an HMAC-SHA256-signed proof:
const consentify = createConsentify({
policy: { categories: ['analytics'] as const },
secret: process.env.CONSENT_PROOF_SECRET,
});
const proof = await consentify.getProof();Typed Event System
consentify.on('change', ({ from, to, timestamp }) => {
console.log('changed', { from, to });
});
consentify.on('clear', ({ timestamp }) => {
console.log('consent cleared');
});
consentify.on('expiring', ({ expiresAt, daysRemaining }) => {
console.log(`expires in ${daysRemaining} days`);
});
// One-shot:
consentify.once('change', () => console.log('first change'));on/once return unsubscribe functions. They are preferred over subscribe when you need typed payloads (from/to snapshots, timestamps).
Server API (SSR)
For reading consent in server code, pass the Cookie header:
import { consentify } from '@/lib/consentify';
export async function Page({ headers }: { headers: Headers }) {
const state = consentify.get(headers.get('cookie') ?? '');
if (state.decision === 'decided' && state.snapshot.choices.analytics) {
// render server-side analytics scripts
}
}get(cookieHeader), set(choices, cookieHeader), clear(cookieHeader) all accept a cookie string and return a Set-Cookie header string for set/clear, or a ConsentState for get.
The same methods are also available as consentify.server.get(cookieHeader) / consentify.server.set(...) if you prefer explicit server namespacing.
Policy Versioning
The SDK generates a deterministic FNV-1a hash of your category keys (or uses your explicit policy.identifier). The hash is stored in the consent cookie.
When you add or remove categories, the hash changes. Existing cookies with the old hash are treated as { decision: 'unset' } - the visitor is prompted again.
consentify.policy.identifier; // e.g. 'a1b2c3d4'
consentify.policy.categories; // readonly ['analytics', 'marketing']Multi-tab Sync
Consent changes sync across browser tabs automatically via BroadcastChannel. Subscribers in all open tabs fire on every set/clear.
Google Consent Mode v2
import { createConsentify, enableConsentMode, defaultConsentModeMapping } from '@consentify/core';
export const consentify = createConsentify({
policy: { categories: ['analytics', 'marketing', 'preferences'] as const },
});
enableConsentMode(consentify, {
mapping: defaultConsentModeMapping,
waitForUpdate: 500, // optional; delays gtag events by N ms to let consent resolve
});enableConsentMode bootstraps window.dataLayer and window.gtag if absent, then calls gtag('consent', 'default', ...) with all types denied, followed by gtag('consent', 'update', ...) whenever consent changes.
Provide a custom mapping to connect your categories to Google consent types:
enableConsentMode(consentify, {
mapping: {
analytics: ['analytics_storage'],
marketing: ['ad_storage', 'ad_user_data', 'ad_personalization'],
preferences: ['functionality_storage', 'personalization_storage'],
},
});Debug Adapter
Tree-shakeable helper that logs every event:
import { enableDebug } from '@consentify/core';
enableDebug(consentify); // logs via console.log
enableDebug(consentify, {
onLog: (msg, event) => logger.info({ event }, msg),
});Script Tag (IIFE Bundle)
@consentify/core ships an IIFE bundle at dist/consentify.iife.min.js for script-tag usage without a bundler. Import via a <script> tag and access window.Consentify (separate namespace from the hosted widget).
TypeScript
Category keys are inferred from policy.categories:
const consentify = createConsentify({
policy: { categories: ['analytics', 'marketing'] as const },
});
const state = consentify.get();
if (state.decision === 'decided') {
state.snapshot.choices.analytics; // boolean
state.snapshot.choices.unknown; // type error
}