# Consenti — Full Documentation
> Open-source Cookie Consent & Consent Management Platform (CMP). Zero external runtime dependencies. Apache 2.0 license.
Consenti is a full-featured CMP built for developers. It ships as two independent npm packages and is a self-hosted, open-source alternative to enterprise CMPs like OneTrust, Cookiebot, and Syrenis Cassie.
- Source: https://github.com/santoshe61/consenti
- npm UI: https://www.npmjs.com/package/@consenti/ui
- npm API: https://www.npmjs.com/package/@consenti/api
- Docs: https://consenti.dev/docs/getting-started/
- Summary (llms.txt): https://consenti.dev/llms.txt
---
## Packages
- `@consenti/ui` — Browser widget. Renders the consent banner and preference modal. CSS is auto-injected at runtime — no separate CSS import needed. Handles GPC signal detection, cross-tab sync via BroadcastChannel, Google Consent Mode v2, GTM integration, i18n (locale resolution), and accessibility (WCAG AAA, focus trap, ARIA roles).
- `@consenti/api` — Node.js backend module. Records consent to SQLite, MongoDB, PostgreSQL, or MySQL via a StorageAdapter interface. Serves a built-in Preact admin dashboard at `/consenti/`. Provides a REST API. Implements RBAC, multiple auth modes (local, OIDC, SAML), immutable audit logs, and GDPR right-to-erasure endpoint.
---
## Key Technical Facts
- Runtime: Node 20+ (backend) / ES2020+ browsers (Chrome 80+, Firefox 74+, Safari 13.1+)
- UI zero external runtime deps: `document.cookie`, `BroadcastChannel`, `crypto.subtle`, `fetch`, `localStorage`
- API zero external runtime deps: `node:sqlite` (Node 22.5+), `node:crypto`, `node:fs`, `node:path`
- Build: tsup (ESM + UMD for UI; CJS + ESM for API), Turborepo monorepo, npm workspaces
- Security: IPs stored as SHA-256 hashes only, passwords via scrypt (native), JWT via HMAC-SHA256 (native), signed consent cookies using HMAC
- User IDs: `crypto.randomUUID()` — no PII stored
- License: Apache 2.0
- Language: TypeScript strict mode throughout
---
## Compliance Coverage
| Regulation | Jurisdiction | Mode | Status |
|---|---|---|---|
| GDPR | EU / EEA | Opt-in | Full |
| UK GDPR | United Kingdom | Opt-in | Full |
| CCPA / CPRA | California, USA | Opt-out | Full |
| VCDPA / CPA / CTDPA | Virginia, Colorado, Connecticut | Opt-out | Full |
| LGPD | Brazil | Opt-in | Full |
| PIPL | China | Opt-in | Full |
| PDPA | Thailand | Opt-in | Full |
| APPI | Japan | Opt-in | Full |
| DPDPA | India | Opt-in | Full |
| POPIA | South Africa | Opt-in | Full |
| PIPEDA / Law 25 | Canada / Quebec | Opt-in | Full |
| KVKK | Turkey | Opt-in | Full |
| TCF v2.2 | IAB / Global | Vendor consent | Full |
| GPC | Global | Signal detection | Full |
| COPPA | USA (children) | Age gate | Partial |
| Notice-only | Any | Informational | Full |
---
## Introduction
Consenti is an open-source, zero-dependency Consent Management Platform (CMP). It handles the complete consent lifecycle — banner display, preference management, GPC detection, GTM / Google Consent Mode v2 integration, and compliance for GDPR, CCPA, CPRA, and ten other regulations.
### Frontend-only vs Full-stack
The UI widget works entirely in the browser with no server needed. The backend is optional and adds server-side consent storage, a profile management dashboard, multi-tenant support, and audit logs.
| | Frontend-only | With backend |
|---|---|---|
| Consent storage | Browser cookie or localStorage | Server database (SQLite, MongoDB, PostgreSQL) |
| Profile management | Defined in code | Admin dashboard — no code changes to update copy |
| Consent audit log | No | Yes — every record stored with timestamp and visitor ID |
| Multi-site / multi-tenant | No | Yes |
| Setup effort | One import, one constructor call | npm install @consenti/api, mounts onto your existing server |
Tip: Start frontend-only. The switch to full-stack later is one config line: `api.enabled: true`.
### What happens on a first visit
1. The widget initialises and resolves the active profile (code-defined, API-fetched, or built-in default)
2. It reads any existing consent record from the browser cookie or localStorage
3. If no valid record exists → the banner appears
4. The user clicks Accept / Reject / Customize
5. Consent is saved to the browser and (if configured) POSTed to the backend
6. `consenti:consentSubmitted` fires with the full consent record
7. On every subsequent visit → the banner stays hidden unless consent has expired or the profile version changed
---
## UI Widget — Installation
### npm (recommended)
```bash
npm install @consenti/ui
```
Import and initialise — no CSS import needed, styles are injected automatically:
```ts
import { ConsentiSetup } from '@consenti/ui'
```
### CDN / UMD (no build step)
```html
```
### ESM in the browser (no bundler)
```html
```
### CSS options
| Approach | How |
|---|---|
| Auto-inject (default) | Nothing to do — ConsentiSetup injects a style tag on first render |
| Preload CSS (optional, avoids FOUC) | `import '@consenti/ui/dist/index.css'` in your bundler entry |
| Disable auto-inject (bring your own styles) | Set `core.disableCssTemplate: true` |
| Token overrides only | Override `--consenti-*` CSS custom properties in your stylesheet |
### Package structure
```
@consenti/ui/
├── dist/
│ ├── index.mjs ← ESM entry
│ ├── index.umd.js ← UMD entry (CDN)
│ ├── index.d.ts ← TypeScript types
│ ├── index.css ← Default styles
│ ├── react.mjs ← React hook
│ ├── vue.mjs ← Vue composable
│ └── angular.mjs ← Angular service
```
---
## UI Widget — Quick Start (Frontend Only)
```bash
npm install @consenti/ui
```
Pass an empty config and Consenti auto-detects the right compliance group from the browser's locale — GDPR for EU visitors, CCPA for California, etc.
```ts
import { ConsentiSetup } from '@consenti/ui'
// No CSS import needed — styles are auto-injected at runtime
const widget = new ConsentiSetup({
compliance: { type: 'auto' }, // geo-based: GDPR for EU, CCPA for CA, etc.
})
```
### Check consent before loading scripts
```ts
widget.onReady((consent) => {
if (consent.analytics) initAnalytics()
if (consent.marketing) initAds()
})
```
### Specific compliance mode
```ts
// GDPR opt-in
new ConsentiSetup({ compliance: { type: 'opt-in' } })
// CCPA opt-out
new ConsentiSetup({ compliance: { type: 'opt-out' } })
// TCF v2.2
new ConsentiSetup({ compliance: { type: 'tcf' } })
```
---
## UI Widget — Quick Start (Frontend + Backend)
### Backend (Node.js / Express)
```bash
npm install @consenti/api
```
```ts
import express from 'express'
import { createConsenti } from '@consenti/api'
const app = express()
const consenti = createConsenti({
storage: { driver: 'node:sqlite', path: './consenti-data' },
auth: {
mode: 'local',
adminEmail: 'admin@example.com',
adminPassword: process.env.CONSENTI_ADMIN_PASSWORD!,
},
dashboard: true,
})
app.use(consenti.router)
app.listen(3000)
// Admin dashboard → http://localhost:3000/consenti/
```
### Frontend — connect to backend
```ts
import { ConsentiSetup } from '@consenti/ui'
const widget = new ConsentiSetup({
compliance: { type: 'auto' },
api: { enabled: true, baseUrl: 'https://your-api.example.com' },
})
```
---
## UI Widget — Framework Integrations
### React
```ts
import { useConsent } from '@consenti/ui/react'
function CookieBanner() {
const { consent, acceptAll, rejectAll } = useConsent()
return consent ? null :
}
```
### Vue 3
```ts
import { useConsent } from '@consenti/ui/vue'
const { consent, acceptAll, rejectAll } = useConsent()
```
### Angular
```ts
import { ConsentiService } from '@consenti/ui/angular'
@Component({ ... })
export class AppComponent {
constructor(private consenti: ConsentiService) {
this.consenti.onReady(consent => { ... })
}
}
```
### Next.js App Router
```tsx
// app/layout.tsx
'use client'
import { useEffect } from 'react'
import { ConsentiSetup } from '@consenti/ui'
export default function RootLayout({ children }) {
useEffect(() => {
new ConsentiSetup({ compliance: { type: 'auto' } })
}, [])
return
{children}
}
```
### Nuxt
```ts
// plugins/consenti.client.ts
import { ConsentiSetup } from '@consenti/ui'
export default defineNuxtPlugin(() => {
new ConsentiSetup({ compliance: { type: 'auto' } })
})
```
---
## UI Widget — DOM Events
All events are `CustomEvent`s fired on `document`:
| Event | When | Detail |
|---|---|---|
| `consenti:bannerInitialized` | Widget ready, banner status known | `{ visible: boolean }` |
| `consenti:bannerVisibility` | Banner shown or hidden | `{ visible: boolean }` |
| `consenti:modalVisibility` | Preference modal shown or hidden | `{ visible: boolean }` |
| `consenti:consentBeingSubmitted` | User clicked accept/reject (before write) | consent object |
| `consenti:consentSubmitted` | Consent saved | full consent record |
```ts
document.addEventListener('consenti:consentSubmitted', (e: CustomEvent) => {
const { analytics, marketing, functional } = e.detail
if (analytics) loadAnalytics()
})
```
---
## UI Widget — API Methods
```ts
const widget = new ConsentiSetup({ ... })
// Read current consent
const consent = widget.getConsent()
// → { analytics: 'granted', marketing: 'denied', functional: 'granted', ... }
// Accept all categories
widget.acceptAll()
// Reject all non-mandatory categories
widget.rejectAll()
// Accept specific categories
widget.acceptCategories(['analytics', 'functional'])
// Open preference modal
widget.openModal()
// Close preference modal
widget.closeModal()
// Check if a category is granted
widget.isGranted('analytics') // → true | false
// Re-show the banner (e.g. after clicking "Cookie Settings")
widget.showBanner()
```
---
## UI Widget — Profiles (Appearance & Text)
Profiles control banner text, button labels, positioning, and styling without touching JavaScript. Create and manage profiles in the admin dashboard, then reference by slug:
```ts
const widget = new ConsentiSetup({
compliance: { type: 'opt-in' },
profileSlug: 'my-profile', // optional — falls back to active default profile
})
```
Or define a profile inline (no backend needed):
```ts
const widget = new ConsentiSetup({
compliance: { type: 'opt-in' },
profileOverride: {
id: 'my-profile',
version: 1,
text: {
bannerTitle: 'We value your privacy',
bannerDescription: 'We use cookies to improve your experience.',
acceptAll: 'Accept All',
rejectAll: 'Reject All',
customize: 'Customize',
},
position: 'bottom-center', // 'bottom-left' | 'bottom-right' | 'bottom-center' | 'top'
categories: [
{ id: 'analytics', name: 'Analytics', description: 'Usage statistics', mandatory: false },
{ id: 'marketing', name: 'Marketing', description: 'Targeted ads', mandatory: false },
],
},
})
```
---
## Backend API — Installation
Node.js 22+ required (Node 24 for `node:sqlite` built-in).
```bash
npm install @consenti/api
```
### Standalone (node:http)
```ts
import { createServer } from 'node:http'
import { createConsenti } from '@consenti/api'
const consenti = createConsenti({
storage: { driver: 'node:sqlite', path: './consenti-data' },
auth: {
mode: 'local',
adminEmail: 'admin@example.com',
adminPassword: process.env.CONSENTI_ADMIN_PASSWORD!,
},
dashboard: true,
})
await consenti.ready
const server = createServer(consenti.handler)
server.listen(3001)
// Admin dashboard → http://localhost:3001/consenti/
// REST API → http://localhost:3001/consenti/api/v1/
```
### Express
```ts
import express from 'express'
import { createConsenti } from '@consenti/api'
const app = express()
const consenti = createConsenti({
storage: { driver: 'node:sqlite', path: './consenti-data' },
auth: { mode: 'local', adminEmail: 'admin@example.com', adminPassword: process.env.CONSENTI_ADMIN_PASSWORD! },
dashboard: true,
})
app.use(consenti.router)
app.listen(3000)
```
### Fastify
```ts
import Fastify from 'fastify'
import { createConsenti } from '@consenti/api'
const fastify = Fastify()
const consenti = createConsenti({ ... })
await fastify.register(consenti.fastifyHandler)
await fastify.listen({ port: 3000 })
```
### Next.js App Router
```ts
// app/consenti/[...path]/route.ts
import { createConsenti } from '@consenti/api'
import type { NextRequest } from 'next/server'
let consenti: Awaited>
async function getConsenti() {
if (!consenti) {
consenti = createConsenti({
storage: { driver: 'node:sqlite', path: './consenti-data' },
auth: { mode: 'local', adminEmail: process.env.CONSENTI_ADMIN_EMAIL!, adminPassword: process.env.CONSENTI_ADMIN_PASSWORD! },
})
}
return consenti
}
export async function GET(req: NextRequest) {
const c = await getConsenti()
return c.nextHandler(req)
}
export const POST = GET
export const DELETE = GET
```
---
## Backend API — Storage Drivers
| Driver | Package | Notes |
|---|---|---|
| `json` | built-in | File-based, dev/small sites |
| `node:sqlite` | built-in (Node 22.5+) | Zero-install SQLite, recommended |
| `node-sqlite3-wasm` | `node-sqlite3-wasm` | WASM SQLite, Node 20+ |
| `better-sqlite3` | `better-sqlite3` | Native SQLite |
| `postgresql` | `pg` | Production recommended |
| `mysql` | `mysql2` | Production recommended |
| `mongodb` | `mongodb` | Production recommended |
```ts
// PostgreSQL
createConsenti({
storage: { driver: 'postgresql', connectionString: process.env.DATABASE_URL! },
})
// MongoDB
createConsenti({
storage: { driver: 'mongodb', uri: process.env.MONGODB_URI!, database: 'consenti' },
})
// MySQL
createConsenti({
storage: { driver: 'mysql', host: 'localhost', database: 'consenti', user: 'root', password: process.env.MYSQL_PASSWORD! },
})
```
---
## Backend API — Auth Modes
### Local (username + password)
```ts
auth: {
mode: 'local',
adminEmail: 'admin@example.com',
adminPassword: process.env.CONSENTI_ADMIN_PASSWORD!,
jwtSecret: process.env.CONSENTI_JWT_SECRET!, // optional, auto-generated if omitted
}
```
### OIDC (SSO — Google, Auth0, Okta, etc.)
```ts
auth: {
mode: 'oidc',
oidc: {
issuer: 'https://accounts.google.com',
clientId: process.env.OIDC_CLIENT_ID!,
clientSecret: process.env.OIDC_CLIENT_SECRET!,
redirectUri: 'https://your-app.example.com/consenti/auth/callback',
},
}
```
### SAML
```ts
auth: {
mode: 'saml',
saml: {
entryPoint: 'https://your-idp.example.com/sso',
issuer: 'consenti',
cert: process.env.SAML_CERT!,
},
}
```
---
## Backend API — REST Routes
### Public Routes (no auth required)
| Method | Path | Description |
|---|---|---|
| GET | `/consenti/api/v1/profile` | Get active profile for a domain |
| POST | `/consenti/api/v1/consent` | Record a consent decision |
| GET | `/consenti/api/v1/consent/:visitorId` | Get consent record for visitor |
| DELETE | `/consenti/api/v1/consent/:visitorId` | GDPR right-to-erasure |
| GET | `/consenti/api/v1/consent/:visitorId/verify` | Verify if consent is still valid |
### Admin Routes (JWT required)
| Method | Path | Description |
|---|---|---|
| GET | `/consenti/api/v1/admin/profiles` | List all profiles |
| POST | `/consenti/api/v1/admin/profiles` | Create profile |
| PUT | `/consenti/api/v1/admin/profiles/:id` | Update profile |
| DELETE | `/consenti/api/v1/admin/profiles/:id` | Delete profile |
| GET | `/consenti/api/v1/admin/consents` | List consent records (paginated) |
| GET | `/consenti/api/v1/admin/audit-logs` | List audit log entries |
| GET | `/consenti/api/v1/admin/stats` | Dashboard statistics |
---
## GDPR Compliance Guide
Consenti is built from the ground up around GDPR requirements (Regulation (EU) 2016/679).
### Key requirements and how Consenti meets them
| Requirement | Implementation |
|---|---|
| Freely given | No content gating; overlay opacity defaults to 0 |
| Specific | Per-cookie granularity — each cookie has its own consent entry |
| Informed | `htmlText` on categories, `legitimateInterest.description` for LI basis |
| Unambiguous | No pre-ticked boxes; all non-mandatory cookies default to `'denied'` |
| Easy to withdraw | `DELETE /consenti/api/v1/consent/:visitorId` (same prominence as granting) |
| Records kept | Immutable `consent_history` table; `audit_logs` for admin actions |
### Right to erasure (Article 17)
```http
DELETE /consenti/api/v1/consent/:visitorId
```
This deletes all entries in `consent_records`, `consent_history`, and the `visitors` record (IP hash, UA hash, geolocation). The visitor ID is a random UUID containing no PII.
### Data minimisation (Article 5(1)(c))
- IP addresses are stored as SHA-256 hashes only
- User agents are stored as SHA-256 hashes only
- Consent records contain only: visitor UUID, profile ID, consent choices, timestamp, source
### Consent records as evidence
Every consent record includes:
- `profile_version` — which version of the cookie policy the user consented to
- `created_at` / `updated_at` — exact ISO 8601 timestamps
- `source` — `'banner'`, `'api'`, or `'import'`
- `consent_history` — immutable append-only log of every change
### Legitimate Interest (Article 6(1)(f))
```json
{
"id": "ad_personalization",
"type": "legitimate_interest",
"legitimateInterest": {
"enabled": true,
"description": "We show relevant ads under legitimate interest."
}
}
```
Status values for LI cookies: `"granted"` (user did not object) or `"objected"` (user exercised Art. 21 right to object).
### Re-consent on profile version bump
When you update a profile (e.g. adding new cookies), the `version` field increments. The widget's `verify` endpoint returns `{ valid: false, reasons: ['profile_version_mismatch'] }` for consents given under an older version, triggering re-consent.
---
## CCPA / US State Privacy Laws
Consenti supports opt-out consent models required by CCPA, VCDPA, CPA, CTDPA, TDPSA, and similar US state laws.
### Opt-out model vs. GDPR opt-in
| | GDPR | CCPA / US States |
|---|---|---|
| Default | All non-mandatory cookies denied | All non-mandatory cookies granted |
| Trigger | Banner on first visit | Silent auto-consent; provide opt-out UI |
| GPC | Optional honour | Required to honour |
### Enabling CCPA mode
```ts
new ConsentiSetup({
compliance: { type: 'opt-out' },
})
```
### GPC — Global Privacy Control
The GPC signal (`navigator.globalPrivacyControl === true`) is treated as an opt-out under CCPA. When `autoHonorGPC: 'strict'` is set:
1. Widget detects GPC signal
2. Automatically denies all `listenGpc: true` cookies
3. Writes consent record immediately without showing banner
4. `gpc_detected: true` is stored on the consent record
### "Do Not Sell" / Opt-out button
```json
{
"text": "Do Not Sell My Data",
"type": "reject",
"cookies": "!"
}
```
The `'!'` action sets all non-mandatory cookies to `'denied'` and writes the consent record.
### State-by-state coverage
| Law | Jurisdiction | Opt-out mechanism |
|---|---|---|
| CCPA / CPRA | California | GPC + reject button |
| VCDPA | Virginia | Opt-out link |
| CPA | Colorado | GPC + opt-out link |
| CTDPA | Connecticut | Opt-out link |
| TDPSA | Texas | Opt-out link |
---
## TCF v2.2 (IAB Transparency & Consent Framework)
TCF v2.2 is required for publishers using programmatic advertising via the IAB's vendor ecosystem.
```ts
new ConsentiSetup({
compliance: { type: 'tcf' },
tcf: {
publisherCC: 'DE', // your country code
purposeOneTreatment: false,
},
})
```
Consenti generates a valid TCF consent string (TC string) and exposes the `__tcfapi` JavaScript API required by ad tech vendors.
### Stacks
TCF v2.2 supports "stacks" — pre-grouped purposes that simplify the consent UI. Consenti supports all 11 official IAB stacks.
---
## Google Consent Mode v2
Consenti pushes consent state to the GTM `dataLayer` automatically when Google Consent Mode v2 is enabled:
```ts
new ConsentiSetup({
compliance: { type: 'opt-in' },
googleConsentMode: { enabled: true },
})
```
This fires `gtag('consent', 'update', { ... })` with `analytics_storage`, `ad_storage`, `ad_user_data`, and `ad_personalization` values derived from the user's consent choices.
---
## Plugins
Plugins extend the backend API to forward consent records to data warehouses and analytics platforms.
### BigQuery
```ts
import { createConsenti } from '@consenti/api'
import { bigQueryPlugin } from '@consenti/api/plugins/bigquery'
createConsenti({
plugins: [
bigQueryPlugin({
projectId: process.env.GCP_PROJECT_ID!,
dataset: 'consent_data',
table: 'consent_records',
}),
],
})
```
### Segment
```ts
import { segmentPlugin } from '@consenti/api/plugins/segment'
createConsenti({
plugins: [
segmentPlugin({
writeKey: process.env.SEGMENT_WRITE_KEY!,
}),
],
})
```
### Snowflake
```ts
import { snowflakePlugin } from '@consenti/api/plugins/snowflake'
createConsenti({
plugins: [
snowflakePlugin({
account: process.env.SNOWFLAKE_ACCOUNT!,
username: process.env.SNOWFLAKE_USER!,
password: process.env.SNOWFLAKE_PASSWORD!,
database: 'CONSENT_DB',
schema: 'PUBLIC',
table: 'CONSENT_RECORDS',
}),
],
})
```
---
## Links
- [Getting Started](https://consenti.dev/docs/getting-started/)
- [Quick Start](https://consenti.dev/docs/getting-started/quick-start/)
- [UI Widget Overview](https://consenti.dev/docs/ui/)
- [UI Installation](https://consenti.dev/docs/ui/installation/)
- [UI Configuration Reference](https://consenti.dev/docs/ui/configuration/)
- [UI Events](https://consenti.dev/docs/ui/events/)
- [UI API Methods](https://consenti.dev/docs/ui/methods/)
- [Themes & CSS Custom Properties](https://consenti.dev/docs/ui/themes/)
- [Framework Guides (React, Vue, Angular, Next.js, Nuxt)](https://consenti.dev/docs/ui/frameworks/)
- [Profiles](https://consenti.dev/docs/ui/profiles/)
- [Backend API Overview](https://consenti.dev/docs/api/)
- [Backend Installation](https://consenti.dev/docs/api/installation/)
- [Backend Configuration Reference](https://consenti.dev/docs/api/configuration/)
- [REST API Routes](https://consenti.dev/docs/api/routes/)
- [Admin Dashboard](https://consenti.dev/docs/api/dashboard/)
- [GDPR Guide](https://consenti.dev/docs/compliance/gdpr/)
- [UK GDPR Guide](https://consenti.dev/docs/compliance/uk-gdpr/)
- [CCPA / US State Privacy Laws](https://consenti.dev/docs/compliance/ccpa/)
- [PDPA Thailand Guide](https://consenti.dev/docs/compliance/pdpa-th/)
- [APPI Japan Guide](https://consenti.dev/docs/compliance/appi/)
- [LGPD Brazil Guide](https://consenti.dev/docs/compliance/lgpd/)
- [PIPL China Guide](https://consenti.dev/docs/compliance/pipl/)
- [DPDPA India Guide](https://consenti.dev/docs/compliance/dpdpa/)
- [POPIA South Africa Guide](https://consenti.dev/docs/compliance/popia/)
- [PIPEDA Canada Guide](https://consenti.dev/docs/compliance/pipeda/)
- [KVKK Turkey Guide](https://consenti.dev/docs/compliance/kvkk/)
- [TCF v2.2 Guide](https://consenti.dev/docs/compliance/tcf/)
- [COPPA Guide](https://consenti.dev/docs/compliance/coppa/)
- [Interactive Playground](https://consenti.dev/demo-playground/frontend)
- [GitHub Repository](https://github.com/santoshe61/consenti)
- [npm — @consenti/ui](https://www.npmjs.com/package/@consenti/ui)
- [npm — @consenti/api](https://www.npmjs.com/package/@consenti/api)