@consentify/core
v2.5.0
Published
Headless cookie consent SDK - zero dependencies, TypeScript-first, SSR-safe.
Maintainers
Readme
@consentify/core
Headless cookie consent SDK — zero dependencies, TypeScript-first, SSR-ready.
Why Consentify?
- 🪶 Lightweight — Zero runtime dependencies, ~2KB minified + gzipped
- 🔒 Type-safe — Full TypeScript support with inference for your categories
- ⚡ SSR-ready — Separate server/client APIs that never touch the DOM on server
- ⚛️ React-ready — Built-in
useSyncExternalStoresupport for React 18+ - 🎯 Headless — Bring your own UI, we handle the state
- 📋 Compliant — Built for GDPR, CCPA, and similar regulations
Install
Fastest path: scaffold the SDK into your project with npx create-consentify@latest.
Or install directly:
npm install @consentify/core
# or
pnpm add @consentify/core
# or
yarn add @consentify/coreQuick Start
import { createConsentify, defaultCategories } from '@consentify/core';
const consent = createConsentify({
policy: {
identifier: 'v1.0',
categories: defaultCategories, // ['preferences', 'analytics', 'marketing', 'functional', 'unclassified']
},
});
// Set user choices
consent.client.set({ analytics: true, marketing: false });
// Check consent
if (consent.isGranted('analytics')) {
loadAnalytics();
}
// Get full state
const state = consent.client.get();
// → { decision: 'decided', snapshot: { policy: '...', givenAt: '...', choices: {...} } }
// → { decision: 'unset' } (if no consent given yet)React Integration
For React projects, use the @consentify/react package which provides a ready-to-use hook:
npm install @consentify/reactimport { createConsentify, defaultCategories, useConsentify } from '@consentify/react';
const consent = createConsentify({
policy: { identifier: 'v1.0', categories: defaultCategories },
});
function CookieBanner() {
const state = useConsentify(consent);
if (state.decision === 'decided') return null;
return (
<div className="cookie-banner">
<p>We use cookies to enhance your experience.</p>
<button onClick={() => consent.acceptAll()}>Accept All</button>
<button onClick={() => consent.rejectAll()}>Essential Only</button>
</div>
);
}If you prefer not to add the React package, you can use useSyncExternalStore directly:
import { useSyncExternalStore } from 'react';
import { createConsentify, defaultCategories } from '@consentify/core';
const consent = createConsentify({
policy: { identifier: 'v1.0', categories: defaultCategories },
});
function useConsent() {
return useSyncExternalStore(
consent.client.subscribe,
consent.client.get,
consent.client.getServerSnapshot
);
}Server-Side Usage
The server API works with raw Cookie headers — perfect for Next.js, Remix, or any Node.js framework:
// Read consent from request
const state = consent.server.get(request.headers.get('cookie'));
if (state.decision === 'decided' && state.snapshot.choices.analytics) {
// User consented to analytics
}
// Set consent (returns Set-Cookie header string)
const setCookieHeader = consent.server.set(
{ analytics: true },
request.headers.get('cookie')
);
response.headers.set('Set-Cookie', setCookieHeader);
// Clear consent
const clearHeader = consent.server.clear();Next.js App Router Example
// lib/consent.ts
import { createConsentify, defaultCategories } from '@consentify/core';
export const consent = createConsentify({
policy: { identifier: 'v1.0', categories: defaultCategories },
});
// app/layout.tsx
import { cookies } from 'next/headers';
import { consent } from '@/lib/consent';
export default async function RootLayout({ children }) {
const cookieStore = await cookies();
const state = consent.server.get(cookieStore.toString());
return (
<html>
<body>
{children}
{state.decision === 'decided' && state.snapshot.choices.analytics && (
<Analytics />
)}
</body>
</html>
);
}Custom Categories
Define your own consent categories with full type safety:
const consent = createConsentify({
policy: {
identifier: 'v1.0',
categories: ['analytics', 'ads', 'personalization'] as const,
},
});
// TypeScript knows your categories!
consent.set({ analytics: true, ads: false });
consent.isGranted('personalization'); // ✓ valid
consent.isGranted('unknown'); // ✗ type errorConfiguration
createConsentify({
policy: {
identifier: 'v1.0', // Recommended: version your policy
categories: defaultCategories,
},
// Consent validity (when to re-prompt user)
consentMaxAgeDays: 365, // Optional: re-consent after N days
// Consent mode: 'opt-in' (GDPR, default) or 'opt-out' (CCPA)
mode: 'opt-in', // 'opt-in' | 'opt-out'
// Days before expiry to emit 'expiring' event (requires consentMaxAgeDays)
expirationWarningDays: 30, // Default: 30
// Cookie storage settings (browser retention)
cookie: {
name: 'consent', // Default: 'consentify'
maxAgeSec: 60 * 60 * 24 * 365, // Default: 1 year (browser storage)
sameSite: 'Lax', // 'Lax' | 'Strict' | 'None'
secure: true, // Forced true when sameSite='None'
path: '/',
domain: '.example.com', // Optional: for cross-subdomain
},
storage: ['cookie'], // ['cookie'] | ['localStorage', 'cookie']
});API Reference
createConsentify(options)
Returns an object with policy, client, and server properties.
client (browser)
| Method | Description |
|--------|-------------|
| get() | Returns ConsentState - { decision: 'decided', snapshot } or { decision: 'unset' } |
| get(category) | Returns boolean - true if category is consented ('necessary' always returns true) |
| set(choices) | Merges choices and persists; notifies subscribers if changed |
| clear() | Removes stored consent; notifies subscribers |
| acceptAll() | Sets all user categories to true |
| rejectAll() | Sets all user categories to false (necessary stays true) |
| getProof() | Returns ConsentProof with tamper-evident signature, or null if unset |
| subscribe(cb) | Subscribe to changes; returns unsubscribe function |
| getServerSnapshot() | Returns { decision: 'unset' } for SSR hydration |
server (Node.js)
| Method | Description |
|--------|-------------|
| get(cookieHeader) | Parse consent from Cookie header string |
| set(choices, cookieHeader?) | Returns Set-Cookie header string |
| clear() | Returns Set-Cookie header string to delete cookie |
Types
type ConsentState<T> =
| { decision: 'unset' }
| { decision: 'decided'; snapshot: Snapshot<T> };
interface Snapshot<T> {
policy: string; // Policy identifier/hash
givenAt: string; // ISO timestamp
choices: Choices<T>; // { necessary: true, ...categories }
}
type Choices<T> = Record<'necessary' | T, boolean>;Default Categories
const defaultCategories = [
'preferences', // User preferences (language, theme)
'analytics', // Analytics and performance
'marketing', // Advertising and marketing
'functional', // Enhanced functionality
'unclassified', // Uncategorized cookies
] as const;Consent Mode (opt-in / opt-out)
// GDPR (default): categories denied until user consents
const gdpr = createConsentify({
policy: { categories: ['analytics'] as const },
mode: 'opt-in',
});
gdpr.isGranted('analytics'); // false (until user consents)
// CCPA: categories granted until user opts out
const ccpa = createConsentify({
policy: { categories: ['analytics'] as const },
mode: 'opt-out',
});
ccpa.isGranted('analytics'); // true (until user opts out)Consent Proof (Audit Trail)
consent.set({ analytics: true, marketing: false });
const proof = consent.getProof();
// { policy: '...', givenAt: '2026-...', choices: {...}, signature: '...' }
// Server-side
const proof = consent.getProof(cookieHeader);Signed vs unsigned. Pass a secret to enable tamper-evident HMAC-SHA256 signatures (recommended for any compliance use case). In signed mode, getProof() returns Promise<ConsentProof<T> | null>.
const consent = createConsentify({
policy: { categories: ['analytics'] as const },
secret: process.env.CONSENT_SIGNING_SECRET,
});
const proof = await consent.getProof();Without a secret, getProof() falls back to a non-cryptographic FNV1a hash. This path is forgeable, emits a one-time console.warn, and is slated to return null in a future major release. Only use it for local debugging.
Expiration Warning
const consent = createConsentify({
policy: { categories: ['analytics'] as const },
consentMaxAgeDays: 365,
expirationWarningDays: 30,
});
consent.on('expiring', (event) => {
console.log(`Consent expires in ${event.daysRemaining.toFixed(0)} days`);
// Show re-consent prompt
});Script Tag / IIFE
For non-bundled apps (WordPress, static sites), load the IIFE build directly:
<script src="https://unpkg.com/@consentify/core/dist/consentify.iife.min.js"></script>
<script>
var consent = Consentify.createConsentify({
policy: { categories: ['analytics', 'marketing'] }
});
consent.guard('analytics', function () {
// Load analytics script
});
</script>The IIFE bundle is ~5kb gzipped and exposes all exports on the Consentify global.
CSP nonce + SRI (recommended)
If your site enforces a strict Content Security Policy, pin a Subresource Integrity hash and forward a nonce from your server template:
<script
src="https://unpkg.com/@consentify/core@2/dist/consentify.iife.min.js"
integrity="sha384-REPLACE_WITH_SRI_HASH"
crossorigin="anonymous"
nonce="%%CSP_NONCE%%"></script>Pair this with a CSP header like script-src 'self' 'nonce-%%CSP_NONCE%%'. Regenerate the SRI hash per pinned version:
openssl dgst -sha384 -binary dist/consentify.iife.min.js | openssl base64 -ASee MDN: Subresource Integrity for the full spec.
How It Works
Policy versioning — Consent is tied to a policy identifier. When you update your policy (change
identifier), previous consent is invalidated.Necessary cookies — The
'necessary'category is alwaystrueand cannot be disabled.Storage — Cookie is the canonical store (works on server). Optionally mirror to
localStoragefor faster client reads.Compact format — Consent is stored as a URL-encoded JSON snapshot in a single cookie.
Consent expiration — Optional
consentMaxAgeDaysinvalidates consent after N days, requiring users to re-consent. This is independent ofcookie.maxAgeSec(which controls how long the browser stores the cookie).
Support
If you find this library useful:
- ⭐ Star the repo on GitHub
- 💖 Sponsor on GitHub
- ☕ Buy me a coffee on Ko-fi
- ☕ Buy me a coffee
License
MIT © 2025 Roman Denysov
