GDPR & IMY 2026 compliant cookie consent Web Component. Shadow DOM encapsulated, framework-agnostic, under 8KB gzipped.
- Web Component — works in any framework (Astro, React, Vue, Angular, plain HTML)
- Shadow DOM — zero style conflicts with your site
- IMY 2026 compliant by default — symmetric buttons, no dark patterns
- Script-gate engine — blocks scripts, iframes, and tracking pixels until consent
- Google Consent Mode V2 — built-in gtag integration
- i18n — English and Swedish included, fully customisable
- 10 integration presets — GA4, GTM, Meta Pixel, LinkedIn, TikTok, Umami, Plausible, Matomo, HubSpot, Hotjar
- Multi-tenant dashboard — manage multiple domains with team collaboration
- Consent proof ledger — immutable records with PDF export via Gotenberg
- Agency mode — manage client organisations with white-label branding
- Billing integration — Mollie payments with subscription lifecycle
- Automated alerts — health scores, scheduled reports, email notifications
- Cookie scanner — automatic third-party cookie detection
- 7.5KB gzipped core bundle
<scriptsrc="https://consent.example.com/loader.js"
data-domain="yourdomain.com"></script>The loader script fetches your published configuration and initialises the banner automatically.
npm install cookieproofimport'cookieproof';constcc=document.querySelector('cookie-consent');cc.configure({language: 'sv',gcmEnabled: true,});Mark scripts you want to gate behind consent:
<!-- Analytics (blocked until user consents to 'analytics') --><scripttype="text/plain" data-consent="analytics"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"></script><!-- Marketing (blocked until user consents to 'marketing') --><scripttype="text/plain" data-consent="marketing">fbq('init','1234567890');fbq('track','PageView');</script><!-- Iframes --><iframedata-consent="marketing"
data-src="https://www.youtube.com/embed/dQw4w9WgXcQ"
style="display:none;"></iframe>When the user grants consent for a category, the engine swaps type="text/plain" to type="text/javascript" and the scripts execute.
cc.configure({// Categories (defaults: necessary, analytics, marketing, preferences)categories: [{id: 'necessary',required: true,enabled: true},{id: 'analytics',required: false,enabled: false},{id: 'marketing',required: false,enabled: false},{id: 'preferences',required: false,enabled: false},],// UIposition: 'bottom',// 'bottom' | 'top' | 'center'theme: 'auto',// 'light' | 'dark' | 'auto'floatingTrigger: 'left',// true | false | 'left' | 'right'// i18nlanguage: 'sv',// 'en' | 'sv' | auto-detect// Storagestorage: 'localStorage',// 'localStorage' | 'cookie'cookieExpiry: 365,// days// Versioning (bump to re-prompt users)revision: 1,// Google Consent Mode V2gcmEnabled: true,// CallbacksonAccept: (consent)=>console.log('Accepted:',consent),onReject: (consent)=>console.log('Rejected:',consent),onChange: (consent,changed)=>console.log('Changed:',changed),});constcc=document.querySelector('cookie-consent');// Actionscc.acceptAll();cc.rejectAll();cc.acceptCategory('analytics');cc.rejectCategory('marketing');// Statecc.getConsent();// ConsentRecord | nullcc.hasConsent('analytics');// boolean// UIcc.showBanner();cc.showPreferences();cc.hide();cc.reset();// Clear consent, show banner// Eventsconstunsub=cc.on('consent:update',(detail)=>{console.log(detail.consent);console.log(detail.changed);});unsub();// unsubscribe| Event | When |
|---|---|
consent:init | Component initialised, consent state loaded |
consent:update | Any consent change |
consent:accept-all | User clicked Accept All |
consent:reject-all | User clicked Reject All |
consent:category:analytics | Analytics category changed |
The API supports two authentication methods:
- Session cookies — The configurator dashboard uses httpOnly cookies with CSRF double-submit tokens. Login via
POST /api/auth/login. - API key — Set
COOKIEPROOF_API_KEYand pass asAuthorization: Bearer <key>. Used for headless/CI integrations.
Public endpoints (config lookup, proof recording, health check) require no authentication.
| Method | Path | Description |
|---|---|---|
GET | /api/config/:domain | Fetch published banner config for a domain |
POST | /api/proof | Record a consent proof (called by the widget) |
GET | /api/health | Health check (database + Gotenberg status) |
| Method | Path | Description |
|---|---|---|
GET | /api/proof | List proofs with pagination and filters |
GET | /api/proof/:id | Get a single proof by UUID |
GET | /api/proof/stats | Aggregate stats + daily breakdown |
GET | /api/proof/export | CSV export of matching proofs |
DELETE | /api/proof | Purge proofs older than a timestamp |
domain— filter by domain (exact match)method— filter by consent method (accept-all,reject-all,custom,gpc)from/to— timestamp range (milliseconds since epoch)limit/offset— pagination (max 500)
Set WEBHOOK_URL to receive a POST notification whenever a new consent proof is recorded:
{
"event": "consent.recorded",
"data": { "id": "...", "domain": "...", "method": "accept-all", "categories": {} },
"timestamp": 1234567890
}Set WEBHOOK_SECRET to include an X-Webhook-Secret header for verification.
git clone <repo-url>cd CookieProof
cp .env.example .env # configure all required variables
docker compose up -d --buildSee .env.example for a complete reference. Key variables:
| Variable | Default | Description |
|---|---|---|
COOKIEPROOF_API_KEY | (empty) | API key for Bearer token authentication |
ALLOWED_ORIGINS | (empty) | Comma-separated allowed CORS origins |
RETENTION_DAYS | 1095 (3 years) | Auto-purge proofs older than this |
WEBHOOK_URL | (empty) | URL to POST consent events to |
WEBHOOK_SECRET | (empty) | Shared secret for webhook verification |
RESEND_API_KEY | (empty) | Resend API key (preferred email provider) |
SMTP_HOST | (empty) | SMTP server for email (fallback) |
ADMIN_EMAIL | (empty) | Super admin email for bootstrap |
MOLLIE_API_KEY | (empty) | Mollie payment API key |
MOLLIE_WEBHOOK_URL | (empty) | Mollie webhook callback URL (HTTPS) |
MOLLIE_REDIRECT_URL | (empty) | Redirect after Mollie checkout (HTTPS) |
GOTENBERG_URL | http://gotenberg:3000 | Gotenberg PDF service URL |
┌─────────────┐ ┌──────────┐ ┌────────────┐ ┌────────────┐
│ Browser │───▶│ nginx │───▶│ Bun API │───▶│ Gotenberg │
│ <cookie- │ │ :8080 │ │ :3100 │ │ :3000 │
│ consent> │ │ /dist/ │ │ /api/* │ │ PDF gen │
│ │ │ /config │ │ SQLite │ │ │
└─────────────┘ └──────────┘ └────────────┘ └────────────┘
│ /loader.js │
│ /configurator/ │
└──────────────────────┘
Services are deployed via Docker Compose with:
- Read-only filesystems and minimal tmpfs mounts
- All capabilities dropped (
cap_drop: ALL) no-new-privilegessecurity option- Memory and CPU resource limits
- Non-root containers (nginx-unprivileged, su-exec user drop)
import{ga4,facebookPixel}from'cookieproof/integrations';cc.configure({categories: [{id: 'necessary',required: true,enabled: true},{id: 'analytics',services: [ga4('G-XXXXX').service],},{id: 'marketing',services: [facebookPixel('1234567890').service],},],gcmEnabled: true,});Available presets: ga4, gtm, facebookPixel, linkedinInsight, tiktokPixel, umami, plausible, matomo, hubspot, hotjar.
CSS custom properties pierce through Shadow DOM:
cookie-consent {
--cc-bg:#ffffff;
--cc-text:#1a1a1a;
--cc-text-secondary:#6b7280;
--cc-border:#e5e7eb;
--cc-btn-primary-bg:#0d9488;
--cc-btn-primary-text:#ffffff;
--cc-btn-secondary-bg:#e5e7eb;
--cc-btn-secondary-text:#374151;
--cc-toggle-on:#0d9488;
--cc-radius:12px;
--cc-font: system-ui, sans-serif;
--cc-z-index:10000;
--cc-max-width:540px;
}::part() selectors are exposed for deeper customisation: banner, preferences, trigger, btn, btn-accept, btn-reject, btn-settings, category, category-toggle.
These are enforced by default — you don't need to configure anything:
- Reject All button on the first layer with equal visual prominence to Accept All
- No pre-ticked category checkboxes
- Persistent floating trigger for changing consent after the banner is dismissed
- Button order: Reject / Settings / Accept (no positional bias)
- Console warning if you override compliance defaults
When gcmEnabled: true, the engine automatically:
- Calls
gtag('consent', 'default', { all signals: 'denied' })on page load - Updates signals when the user makes a choice:
analyticscategory →analytics_storagemarketingcategory →ad_storage,ad_user_data,ad_personalizationpreferencescategory →functionality_storage,personalization_storage
You can override the mapping with gcmMapping.
When respectGPC is true (default) and navigator.globalPrivacyControl === true:
- Non-essential categories are automatically rejected
- No banner is shown (the floating trigger appears for manual override)
- A
consent:gpcevent is emitted - The proof record has
method: 'gpc'
Set geoEndpoint to skip the consent banner for visitors outside regulated regions:
cc.configure({geoEndpoint: 'https://your-api.com/geo'});The endpoint must return JSON: { "requiresConsent": true | false }. HTTPS is required. If the endpoint fails or is unreachable, the banner is shown (fail-safe).
Copy .env.example to .env, fill the values (JWT_SECRET is
required), then docker compose -f deploy/docker-compose.yml up -d. The web
container serves the widget + dashboard on port 8080; edit nginx.conf's
server_name to your own domain.
CookieProof is fair-code, licensed under the CookieProof Sustainable Use License: self-host free, use it commercially for your own business and your own clients' sites, serve every visitor; you may not resell it as a hosted consent-management service. Versions up to 2026-04-25 were MIT and those copies remain MIT. See LICENSE and LICENSING.md.