@consenti/api 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,12 +8,12 @@ Zero-dependency, GDPR-compliant Consent Management Platform backend for Node.js.
8
8
 
9
9
  ---
10
10
 
11
- ## Why this package?
11
+ ## Why Consenti?
12
12
 
13
13
  - **Three SQLite drivers** — `node:sqlite` (Node 22.5+ built-in, zero install), `node-sqlite3-wasm` (WASM, zero compilation, optional peer dep), `better-sqlite3` (native, optional peer dep)
14
14
  - **One function to mount** — `createConsenti()` returns a router you drop into any Node.js HTTP framework
15
15
  - **Self-contained admin dashboard** — full SPA served at your `basePath`, no separate deploy
16
- - **Multi-regulation** — GDPR, CCPA, CPRA, GPC, TCF v2.2, DPDPA, LGPD, PDPA, PIPEDA, APPI, KVKK, POPIA, UK-GDPR
16
+ - **Multi-regulation** — 76 regulations across EU/EEA, UK, US (22 state laws), India, Brazil, China, Canada, 18 APAC countries, 9 Middle East countries, 14 African countries, and more — with automatic geo-routing via 8 compliance groups
17
17
  - **Pluggable storage** — SQLite (zero-config, default) · PostgreSQL · MySQL · MongoDB
18
18
 
19
19
  ---
@@ -22,7 +22,45 @@ Zero-dependency, GDPR-compliant Consent Management Platform backend for Node.js.
22
22
 
23
23
  - **Node.js ≥ 20.0.0** — Node 22 or 24 LTS recommended for production
24
24
 
25
- ## Which SQLite driver should I use?
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ npm install @consenti/api
30
+ ```
31
+
32
+ ### Database / Storage
33
+ Consenti by default comes with JSON file based storage, to get started without any installation.
34
+ We have inbuilt SQLite, PostgreSQL, MySQL and MongoDB as storage adaptors, all are optional peer dependencies, only install any one of them that you need. Thats not all, you can create an adaptor for any other database you want to use by using [adapter.interface.ts](apps/api/src/storage/adapter.interface.ts)
35
+
36
+ > **Note:** Default JSON file based storage is only for development and small scale deployments, for production it is recommended to use other database options.
37
+
38
+ ```bash
39
+ # Databases (choose any one )
40
+
41
+ # 1. node:sqlite - Node native SQLite,
42
+ # needs no install on Node 22.5+
43
+
44
+ # OR 2. WASM SQLite — Node 20+, zero compilation (optional, install only if storage.driver: "node-sqlite3-wasm")
45
+ npm install node-sqlite3-wasm
46
+
47
+ # OR 3. native SQLite — Node 20+, faster but needs a C++ toolchain (optional, install only if storage.driver: "better-sqlite3")
48
+ npm install better-sqlite3
49
+
50
+ # OR 4. PostgreSQL (optional, install only if storage.driver: "postgresql")
51
+ npm install pg
52
+
53
+ # OR 5. MySQL (optional, install only if storage.driver: "mysql")
54
+ npm install mysql2
55
+
56
+ # OR 6. MongoDB (optional, install only if storage.driver: "mongodb")
57
+ npm install mongodb
58
+
59
+ # OR 7. Create your own custom database Adaptor using "apps/api/src/storage/adapter.interface.ts"
60
+
61
+ ```
62
+
63
+ ### Which SQLite driver should I use?
26
64
 
27
65
  | Driver | Config | Node requirement | Extra install? |
28
66
  |---|---|---|---|
@@ -34,29 +72,178 @@ Zero-dependency, GDPR-compliant Consent Management Platform backend for Node.js.
34
72
  `node:sqlite` is the only driver that requires no extra install — it ships with Node 22.5+.
35
73
  On Node 20/21, choose between `node-sqlite3-wasm` (pure WASM, no compilation) or `better-sqlite3` (native binary, faster but may fail on Alpine/musl/ARM).
36
74
 
37
- ---
38
75
 
39
- ## Installation
76
+ ### Geo resolvers - compliance routing
40
77
 
41
- ```bash
42
- npm install @consenti/api
43
- ```
78
+ Consenti ships with four geo resolver options. The default requires zero installation; the others are opt-in.
79
+
80
+ **Bonus:** You can also supply a custom resolver function via `compliance.geoDataProvider` — it receives `{ ip, timezone, language }` and must return `{ country: string | null, region: string | null, locale: string | null }`.
81
+
82
+ > **Note:** The default (`'default'`) and `'hosted-geoip-lite'` resolvers are suitable for development and lower-traffic deployments. For high-accuracy production use, choose `'geoip'` or `'maxmind'`.
83
+
84
+ ### Which geo resolver should I use?
44
85
 
45
- All SQLite drivers and other database adapters are optional peer dependencies — only install what you need:
86
+ | Resolver | Config | Extra install? | Notes |
87
+ |---|---|---|---|
88
+ | `default` | `geoDataProvider: 'default'` | **No** | Timezone + Accept-Language heuristic; zero deps; less accurate |
89
+ | `hosted-geoip-lite` | `geoDataProvider: 'hosted-geoip-lite'` | **No** | Calls `ipinfo.io` via `node:https`; requires outbound internet |
90
+ | `geoip` | `geoDataProvider: 'geoip'` | Yes — `npm install geoip-lite` | Local MaxMind GeoLite2 DB via `geoip-lite`; fast, no outbound traffic |
91
+ | `maxmind` | `geoDataProvider: 'maxmind'` | Yes — `npm install maxmind` | Official MaxMind SDK; most accurate; requires `.mmdb` file |
92
+ | `CountryResolverFn` | `geoDataProvider: myFn` | Depends on your impl | Bring your own resolver — see custom example below |
46
93
 
47
94
  ```bash
48
- # SQLite (choose one — node:sqlite needs no install on Node 22.5+)
49
- npm install node-sqlite3-wasm # WASM SQLite — Node 20+, zero compilation
50
- npm install better-sqlite3 # native SQLite — Node 20+, faster but needs a C++ toolchain
51
-
52
- # Other databases
53
- npm install pg # PostgreSQL
54
- npm install mysql2 # MySQL
55
- npm install mongodb # MongoDB
56
- ```
95
+ # Geo resolvers (choose any one )
96
+
97
+ # 1. Default - language+timezone based
98
+ # No install needed
99
+
100
+ # OR 2. Hosted Geo-IP (optional — only needed when geoDataProvider: 'hosted-geoip-lite')
101
+ # No install needed — calls ipinfo.io using Node's built-in node:https
102
+ # Requires outbound internet access from your server
57
103
 
104
+ # OR 3. Geo-IP lite (optional — only needed when geoDataProvider: 'geoip')
105
+ npm install geoip-lite
106
+
107
+ # OR 4. Maxmind (optional — only needed when geoDataProvider: 'maxmind')
108
+ npm install maxmind
109
+
110
+ # OR 5. Custom (optional — only needed when geoDataProvider: CountryResolverFn)
111
+ # Do what it requires, just return `{ country, region, locale }` in supported format
112
+
113
+ ```
58
114
  ---
59
115
 
116
+
117
+ ## Full Configuration
118
+ Selected values are default
119
+
120
+ ```ts
121
+ import { createConsenti } from '@consenti/api'
122
+
123
+ const consenti = createConsenti({
124
+ // ── Storage ─────────────────────────────────────────────────────────────────
125
+ storage: {
126
+ driver: 'json', // 'json' | 'node:sqlite' | 'node-sqlite3-wasm' | 'better-sqlite3' | 'sqlite' | 'postgresql' | 'mysql' | 'mongodb'
127
+ path: './consenti-data', // base directory — Consenti creates db/, profiles/, logs/ subdirectories automatically
128
+
129
+ // Optional
130
+ uri: 'mongodb://localhost:27017/consenti', // db connection string/uri, only for (driver: 'postgresql' | 'mysql' | 'mongodb')
131
+ database: 'consenti', // database name when not in above uri
132
+ },
133
+
134
+ // ── Auth ─────────────────────────────────────────────────────────────────────
135
+ auth: {
136
+ mode: 'local', // 'local' | 'jwt' | 'oidc' | 'saml' | 'custom'
137
+ adminEmail: 'user@consenti.dev',
138
+ adminPassword: process.env.CONSENTI_ADMIN_PASSWORD ?? "Consenti@123",
139
+ jwtSecret: process.env.CONSENTI_ADMIN_JWT_SECRET, // auto-generated if omitted (sessions expire on restart)
140
+
141
+ // OIDC (Auth0, Keycloak, Google, etc.)
142
+ // mode: 'oidc',
143
+ // oidc: {
144
+ // issuer: 'https://your-idp.example.com',
145
+ // clientId: 'your-client-id',
146
+ // clientSecret: process.env.OIDC_SECRET,
147
+ // redirectUri: 'https://app.example.com/consenti/admin/auth/oidc/callback',
148
+ // claimsMapping: { email: 'email', roles: 'consenti_roles' },
149
+ // },
150
+
151
+ // SAML
152
+ // mode: 'saml',
153
+ // saml: {
154
+ // issuer: 'https://idp.example.com',
155
+ // entryPoint: 'https://idp.example.com/sso/saml',
156
+ // cert: process.env.SAML_CERT!, // IdP signing cert (PEM, no headers)
157
+ // callbackUrl: 'https://app.example.com/consenti/admin/auth/saml/callback',
158
+ // },
159
+
160
+ // Custom — bring your own authentication function
161
+ // mode: 'custom',
162
+ // validateUser: async (req: Request) => AdminUser | null,
163
+ },
164
+
165
+ // ── Routing ──────────────────────────────────────────────────────────────────
166
+ basePath: '/consenti', // URL prefix for all routes (default: '/consenti')
167
+ dashboard: true, // serve built-in admin SPA at basePath (default: true)
168
+
169
+ // ── Branding ─────────────────────────────────────────────────────────────────
170
+ branding: {
171
+ appName: 'Consenti', // shown in dashboard header, login page, browser tab
172
+ appLogoPath: './logo-dark.svg', // local file path or https:// URL; auto-served as static asset
173
+ hidePoweredBy: false, // true = hide "Powered by Consenti" badge
174
+ },
175
+
176
+ // ── Rate limiting ─────────────────────────────────────────────────────────────
177
+ rateLimit: {
178
+ enabled: true,
179
+ windowMs: 60_000, // rolling window in ms (default: 60 000)
180
+ maxRequests: 60, // max requests per IP per window (default: 60)
181
+ },
182
+
183
+ // ── Compliance ───────────────────────────────────────────────────────────────
184
+ compliance: {
185
+ // Geo-based auto routing (recommended)
186
+ type: 'auto', // 'auto' = geo-resolve per visitor | ComplianceGroupId = one fixed group globally
187
+ geoDataProvider: 'default', // 'default' (timezone+language heuristic, zero-dep) | 'hosted-geoip-lite' (no install, calls ipinfo.io) | 'geoip' (optional peer dep) | 'maxmind' (optional peer dep) | CountryResolverFn
188
+ autoComplianceMap: 'default',// 'default' = embedded 240-country map | operator object | 'auto' = fetch from complianceMapUrl
189
+ // complianceMapUrl: 'https://consenti.dev/data/v1/compliance-map.json', // used when autoComplianceMap: 'auto'
190
+ },
191
+
192
+ // ── S3 profile file sync (optional) ─────────────────────────────────────────
193
+ // When enabled, every locale JSON written to disk is also PUT to S3.
194
+ // /resolve-profile returns an s3:// URL so the widget can fetch directly from CDN.
195
+ s3Api: {
196
+ enabled: false,
197
+ region: 'us-east-1',
198
+ bucketName: 'consenti-profiles',
199
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
200
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
201
+ // sessionToken: process.env.AWS_SESSION_TOKEN, // for temporary credentials
202
+ },
203
+
204
+ // ── Cache invalidation hook (optional) ──────────────────────────────────────
205
+ // Called after every profile creation, edit, activation, deactivation, or delete.
206
+ // isPurge=false = warm the cache at these paths; isPurge=true = purge.
207
+ handleCache: (paths: string[], version: number, isPurge: boolean) => {
208
+ // paths: array of file paths that were written or removed
209
+ // version: current profile version
210
+ // isPurge: true when files were removed (deactivate/delete); false when written (activate/create)
211
+ // Example: purge CDN edge nodes, update nginx cache keys, etc.
212
+ },
213
+
214
+ // ── IAB TCF v2.2 ─────────────────────────────────────────────────────────────
215
+ tcf: {
216
+ enabled: false,
217
+ cmpId: 123, // your IAB-registered CMP ID (required when enabled)
218
+ cmpVersion: 1, // your CMP software version
219
+ },
220
+
221
+ // ── Age gate ─────────────────────────────────────────────────────────────────
222
+ ageGate: {
223
+ enabled: false,
224
+ minimumAge: 13, // COPPA = 13; GDPR Article 8 = 16
225
+ requireParentalConsent: false,
226
+ },
227
+
228
+ // ── Data retention ───────────────────────────────────────────────────────────
229
+ dataRetention: {
230
+ purgeAfterDays: 365, // delete consent records older than N days; runs nightly
231
+ },
232
+
233
+ // ── Multi-tenant ─────────────────────────────────────────────────────────────
234
+ multiTenant: {
235
+ enabled: false, // scope all data to tenant from X-Tenant-ID header
236
+ },
237
+
238
+ // ── Proxies & body ───────────────────────────────────────────────────────────
239
+ trustedProxies: [], // IP / CIDR of reverse proxies (reads X-Forwarded-For)
240
+ maxBodySize: 1_048_576, // request body size limit in bytes (default: 1 MB)
241
+
242
+ // ── Plugins ──────────────────────────────────────────────────────────────────
243
+ plugins: [], // ConsentiServerPlugin instances
244
+ })
245
+ ```
246
+
60
247
  ## Quick Start
61
248
 
62
249
  ### Express
@@ -68,7 +255,7 @@ import { createConsenti } from '@consenti/api'
68
255
  const app = express()
69
256
 
70
257
  const consenti = createConsenti({
71
- storage: { driver: 'sqlite', path: './consenti.db' },
258
+ storage: { driver: 'node:sqlite', path: './consenti-data' },
72
259
  auth: {
73
260
  mode: 'local',
74
261
  adminEmail: 'admin@example.com',
@@ -89,10 +276,11 @@ app.listen(3000, () => {
89
276
  ### Standalone (no framework)
90
277
 
91
278
  ```ts
279
+ import { createServer } from 'node:http'
92
280
  import { createConsenti } from '@consenti/api'
93
281
 
94
282
  const consenti = createConsenti({
95
- storage: { driver: 'sqlite', path: './consenti.db' },
283
+ storage: { driver: 'node:sqlite', path: './consenti-data' },
96
284
  auth: {
97
285
  mode: 'local',
98
286
  adminEmail: 'admin@example.com',
@@ -101,9 +289,13 @@ const consenti = createConsenti({
101
289
  dashboard: true,
102
290
  })
103
291
 
104
- await consenti.start(3001)
105
- // Admin dashboard → http://localhost:3001/consenti/
106
- // REST API → http://localhost:3001/consenti/api/v1/
292
+ await consenti.ready // wait for DB + bootstrap
293
+
294
+ const server = createServer(consenti.handler)
295
+ server.listen(3001, () => {
296
+ console.log('Admin dashboard → http://localhost:3001/consenti/')
297
+ console.log('REST API → http://localhost:3001/consenti/api/v1/')
298
+ })
107
299
  ```
108
300
 
109
301
  ---
@@ -122,14 +314,13 @@ app.use(consenti.handler) // as a terminal handler (no next())
122
314
 
123
315
  ```ts
124
316
  import Fastify from 'fastify'
125
- const app = Fastify()
317
+ import { createConsenti } from '@consenti/api'
126
318
 
319
+ const fastify = Fastify()
127
320
  const consenti = createConsenti({ /* ... */ })
128
321
 
129
- app.all('/consenti/*', async (req, reply) => {
130
- reply.hijack()
131
- consenti.handler(req.raw, reply.raw)
132
- })
322
+ await fastify.register(consenti.fastifyHandler)
323
+ await fastify.listen({ port: 3000 })
133
324
  ```
134
325
 
135
326
  ### Next.js App Router
@@ -144,7 +335,7 @@ let consenti: Awaited<ReturnType<typeof createConsenti>>
144
335
  async function getConsenti() {
145
336
  if (!consenti) {
146
337
  consenti = createConsenti({
147
- storage: { driver: 'sqlite', path: './consenti.db' },
338
+ storage: { driver: 'sqlite', path: './consenti-data' },
148
339
  auth: {
149
340
  mode: 'local',
150
341
  adminEmail: process.env.CONSENTI_ADMIN_EMAIL!,
@@ -155,12 +346,12 @@ async function getConsenti() {
155
346
  return consenti
156
347
  }
157
348
 
158
- export async function GET(req: NextRequest) {
349
+ async function consentiHandler(req: NextRequest) {
159
350
  const c = await getConsenti()
160
351
  return c.handleRequest(req)
161
352
  }
162
353
 
163
- export { GET as POST, GET as PUT, GET as DELETE, GET as PATCH }
354
+ export { consentiHandler as POST, consentiHandler as PUT, consentiHandler as DELETE, consentiHandler as PATCH, consentiHandler as GET }
164
355
  ```
165
356
 
166
357
  ### Hono / Fetch-based
@@ -171,133 +362,105 @@ return consenti.honoApp.fetch(request)
171
362
 
172
363
  ---
173
364
 
174
- ## Full Configuration
365
+ ## First Boot
175
366
 
176
- ```ts
177
- import { createConsenti } from '@consenti/api'
367
+ On first start with JSON, Consenti:
178
368
 
179
- const consenti = createConsenti({
180
- // ── Required ────────────────────────────────────────────────────────────────
181
- storage: {
182
- driver: 'node-sqlite3-wasm', // 'node-sqlite3-wasm' | 'node:sqlite' | 'better-sqlite3' | 'pg' | 'mongodb' | 'mysql'
183
- path: './consenti.db', // SQLite only — file path
184
-
185
- // PostgreSQL
186
- // driver: 'pg',
187
- // url: 'postgresql://user:pass@localhost:5432/consenti',
188
- // pool: { max: 10, idleTimeoutMillis: 30000 },
189
-
190
- // MongoDB
191
- // driver: 'mongodb',
192
- // url: 'mongodb://localhost:27017',
193
- // dbName: 'consenti',
194
-
195
- // MySQL
196
- // driver: 'mysql',
197
- // url: 'mysql://user:pass@localhost:3306/consenti',
198
- },
199
- auth: {
200
- mode: 'local', // 'local' | 'oauth' | 'saml' | 'none'
201
- adminEmail: 'admin@example.com',
202
- adminPassword: process.env.CONSENTI_ADMIN_PASSWORD!,
203
- jwtSecret: process.env.CONSENTI_ADMIN_JWT_SECRET, // auto-generated if omitted (changes on restart)
204
- sessionTtl: 86400, // JWT TTL in seconds (default: 24 h)
369
+ 1. Creates the storage directory at the configured path (see [Storage Directory Structure](#storage-directory-structure))
370
+ 2. Runs all migrations (creates `visitors`, `consent_records`, `consent_history`, `profiles`, `admin_users`, `audit_logs` tables, and DB indexes)
371
+ 3. Creates the admin user from `auth.adminEmail` + `auth.adminPassword`
372
+ 4. Seeds the default profile (`profileId: '0'`)
205
373
 
206
- // OAuth / OIDC (e.g. Auth0, Keycloak)
207
- // mode: 'oauth',
208
- // oidc: {
209
- // issuer: 'https://your-idp.example.com',
210
- // clientId: 'your-client-id',
211
- // clientSecret: process.env.OIDC_SECRET,
212
- // redirectUri: 'https://app.example.com/consenti/admin/auth/callback',
213
- // },
374
+ Subsequent starts run only pending migrations.
214
375
 
215
- // SAML
216
- // mode: 'saml',
217
- // saml: {
218
- // issuer: 'consenti',
219
- // entryPoint: 'https://idp.example.com/sso/saml',
220
- // cert: process.env.SAML_CERT,
221
- // callbackUrl: 'https://app.example.com/consenti/admin/auth/saml/callback',
222
- // },
376
+ ---
223
377
 
224
- // Custom — bring your own validator
225
- // mode: 'custom',
226
- // validateUser: async (req) => ({ id, email, roles, permissions } | null),
227
- },
378
+ ## Storage Directory Structure
228
379
 
229
- // ── Optional ────────────────────────────────────────────────────────────────
230
- basePath: '/consenti', // URL prefix (default: '/consenti')
231
- dashboard: true, // serve admin SPA (default: false)
232
- trustProxy: false, // true when behind nginx / Cloudflare
380
+ `storage.path` is a **directory path**. Consenti creates the following layout automatically:
233
381
 
234
- rateLimit: {
235
- enabled: true,
236
- windowMs: 60_000, // rolling window (default: 60 s)
237
- maxRequests: 60, // max requests per IP per window (default: 60)
238
- },
382
+ ```
383
+ ${storage.path}/
384
+ db/
385
+ consenti-data.json # JSON adapter
386
+ consenti.db # SQLite adapters
387
+ profiles/
388
+ ${tenantId}/
389
+ ${profileId}/
390
+ 1/ # version 1 (immutable once written)
391
+ en.json
392
+ fr-FR.json
393
+ default.json # copy of defaultLocale content (fallback)
394
+ 2/ # version 2 written on next save
395
+ …
396
+ ${complianceGroup}/ # hot-serve path (active profile only)
397
+ en.json
398
+ fr-FR.json
399
+ default.json
400
+ logs/ # reserved for future structured logging
401
+ ```
239
402
 
240
- cors: {
241
- origins: ['https://yoursite.com'], // never use '*' in production
242
- credentials: true,
243
- },
403
+ **Key rules:**
404
+ - Every profile save writes to a new immutable version directory under `${profileId}/${version}/`
405
+ - `${complianceGroup}/` only exists when a profile is **active** for that group
406
+ - On **activate**: locale files are copied from `${profileId}/${currentVersion}/` → `${complianceGroup}/`
407
+ - On **deactivate** / **delete**: `${complianceGroup}/` files are removed
408
+ - `default.json` = full resolved profile for `defaultLocale`; used as locale fallback (303 redirect)
244
409
 
245
- compliance: {
246
- gdpr: true, // opt-in enforcement
247
- ccpa: false, // opt-out model
248
- gpc: true, // store gpc_detected on records
249
- tcf: {
250
- enabled: false, // IAB TCF v2.2
251
- cmpId: 123, // your registered CMP ID
252
- },
253
- },
410
+ **Backward compat:** If `storage.path` has a file extension (`.json`, `.db`), the parent directory is used and a deprecation warning is logged.
254
411
 
255
- ageGate: {
256
- enabled: false,
257
- minimumAge: 13,
258
- requireParentalConsent: false,
259
- },
412
+ ---
260
413
 
261
- dataRetention: {
262
- purgeAfterDays: 365, // auto-purge old records (0 = disabled)
263
- },
414
+ ## Public REST API
264
415
 
265
- multiTenant: {
266
- enabled: false, // isolate data per tenant
267
- },
416
+ Base path: `/consenti/api/v1` (change with `basePath` in config). No authentication required.
268
417
 
269
- plugins: [], // ConsentiServerPlugin instances
270
- })
418
+ | Method | Path | Description |
419
+ |------------|---------------------------------------------|------------------------------------------------------------------------|
420
+ | `GET` | `/profiles/:tenantId/:complianceGroup/:locale` | **Hot-serve** — serve locale JSON directly from disk (zero DB) |
421
+ | `GET` | `/profiles/:tenantId/:profileId/:version/:locale` | Serve a specific version locale JSON (dashboard preview) |
422
+ | `GET` | `/resolve-profile` | Geo-resolve compliance group, return file path + locale |
423
+ | `GET` | `/profiles/:id` | get profile by ID (resolved, DB-backed, default tenant, default locale)|
424
+ | `GET` | `/profiles/:id/:locale` | get profile by ID & locale (resolved, DB-backed, default tenant) |
425
+ | `GET` | `/profiles/auto/:locale` | Legacy: geo-resolve then return matching active profile |
426
+ | `POST` | `/consent` | Submit a consent record |
427
+ | `GET` | `/consent/:visitorId` | Get current consent for a visitor |
428
+ | `PUT` | `/consent/:visitorId` | Update consent for a visitor |
429
+ | `DELETE` | `/consent/:visitorId` | GDPR right-to-erasure — delete all visitor data |
430
+ | `GET` | `/consent/:visitorId/verify` | Check if consent is still valid |
431
+
432
+ ### Static file serving (hot path)
433
+
434
+ ```http
435
+ GET /consenti/api/v1/profiles/default/opt-in/en
271
436
  ```
272
437
 
273
- ---
438
+ Serves `${storage.path}/profiles/default/opt-in/en.json` directly from disk. No DB involved.
274
439
 
275
- ## First Boot
440
+ If the locale file doesn't exist, returns `303 See Other → .../opt-in/default` (which serves `default.json` = `defaultLocale` content).
276
441
 
277
- On first start with SQLite, Consenti:
442
+ Cache headers: `public, max-age=3600, stale-while-revalidate=60` — suitable for CDN caching.
278
443
 
279
- 1. Creates the database file at the configured path
280
- 2. Runs all migrations (creates `visitors`, `consent_records`, `consent_history`, `profiles`, `admin_users`, `audit_logs` tables)
281
- 3. Creates the admin user from `auth.adminEmail` + `auth.adminPassword`
282
- 4. Seeds the default profile (`profileId: '0'`)
444
+ ### Resolve profile (for `compliance.type: 'auto'`)
283
445
 
284
- Subsequent starts run only pending migrations.
446
+ ```http
447
+ GET /consenti/api/v1/resolve-profile?tz=Europe/Paris&lang=fr-FR&locale=fr-FR
448
+ ```
285
449
 
286
- ---
450
+ For widgets using `compliance.type: 'auto'` — call this once on page load to discover which compliance group and locale the visitor should receive:
287
451
 
288
- ## Public REST API
289
-
290
- Base path: `/consenti/api/v1` (change with `basePath` in config). No authentication required.
452
+ ```json
453
+ {
454
+ "path": "/consenti/api/v1/profiles/default/opt-in/fr-FR",
455
+ "resolvedLocale": "fr-FR",
456
+ "resolvedComplianceGroup": "opt-in",
457
+ "profileId": "gdpr-profile-uuid",
458
+ "version": 3,
459
+ "warning": "locale_not_found" // only present when locale was unavailable; path points to default
460
+ }
461
+ ```
291
462
 
292
- | Method | Path | Description |
293
- |------------|-------------------------------|-------------------------------------------------|
294
- | `GET` | `/profiles/:id` | Get profile resolved in its default locale |
295
- | `GET` | `/profiles/:id/:locale` | Get profile resolved for a specific locale |
296
- | `POST` | `/consent` | Submit a consent record |
297
- | `GET` | `/consent/:visitorId` | Get current consent for a visitor |
298
- | `PUT` | `/consent/:visitorId` | Update consent for a visitor |
299
- | `DELETE` | `/consent/:visitorId` | GDPR right-to-erasure — delete all visitor data |
300
- | `GET` | `/consent/:visitorId/verify` | Check if consent is still valid |
463
+ Query params: `tz` (IANA timezone), `lang` (Accept-Language value), `locale` (preferred BCP 47 locale).
301
464
 
302
465
  ### Submit consent
303
466
 
@@ -380,13 +543,114 @@ Content-Type: application/json
380
543
 
381
544
  ### Profiles
382
545
 
383
- | Method | Path | Description |
384
- |------------|-------------------|------------------------------------------|
385
- | `GET` | `/profiles` | List all profiles |
386
- | `POST` | `/profiles` | Create a profile |
387
- | `GET` | `/profiles/:id` | Get a profile |
388
- | `PUT` | `/profiles/:id` | Update a profile (auto-bumps version) |
389
- | `DELETE` | `/profiles/:id` | Delete a profile |
546
+ | Method | Path | Description |
547
+ |------------|----------------------------------|-------------------------------------------------------------------------|
548
+ | `GET` | `/profiles` | List all profiles |
549
+ | `GET` | `/profiles?summary=1` | List profiles as `ProfileSummary[]` (no blob, with template names) |
550
+ | `POST` | `/profiles` | Create a profile — 422 on compliance errors; `{ conflict, requiresChoice: true }` when an active profile exists on the same `complianceGroup` |
551
+ | `GET` | `/profiles/:id` | Get a profile |
552
+ | `PUT` | `/profiles/:id` | Update a profile (auto-bumps version) — same compliance validation + conflict detection |
553
+ | `DELETE` | `/profiles/:id` | Delete a profile |
554
+ | `POST` | `/profiles/:id/activate` | Activate a profile — writes locale JSON files to `${complianceGroup}/` |
555
+ | `POST` | `/profiles/:id/deactivate` | Deactivate a profile — removes `${complianceGroup}/` locale files |
556
+ | `GET` | `/profiles/:id/versions` | List version history entries (`{ version, createdAt, locales[] }[]`) |
557
+ | `GET` | `/profiles/:id/versions/:n` | Read a specific version locale file (query: `?locale=en`) |
558
+ | `POST` | `/profiles/validate` | Validate cookie template against a compliance group (no save) |
559
+ | `GET` | `/compliance-coverage` | Active profile (or null) per compliance group — powers coverage panel |
560
+
561
+ #### Profile Save Conflict Detection
562
+
563
+ Only one profile per `complianceGroup` can be active at a time. If you attempt to create or update a profile with `isActive: true` and another active profile already exists for the same `complianceGroup`, the backend returns **200** (not an error) with:
564
+
565
+ ```json
566
+ {
567
+ "conflict": { "id": "existing-profile-uuid", "name": "GDPR Profile v1" },
568
+ "requiresChoice": true
569
+ }
570
+ ```
571
+
572
+ Re-submit the original request with a `choice` field to resolve:
573
+
574
+ ```json
575
+ { "choice": "deactivate" } // deactivate the conflicting profile, then save this one as active
576
+ { "choice": "inactive" } // save this profile as inactive (no conflict)
577
+ ```
578
+
579
+ The dashboard profile editor shows a three-button popup when `requiresChoice: true` — "Deactivate existing and activate this one", "Save as inactive", and "Cancel".
580
+
581
+ #### ProfileSummary shape
582
+
583
+ ```ts
584
+ {
585
+ id: string
586
+ name: string
587
+ defaultLocale: string
588
+ complianceGroup: ComplianceGroupId | null
589
+ version: number
590
+ isActive: boolean
591
+ cookieTemplateName: string | null
592
+ uiTemplateName: string | null
593
+ createdAt: string
594
+ updatedAt: string
595
+ }
596
+ ```
597
+
598
+ ### Cookie Templates
599
+
600
+ | Method | Path | Description |
601
+ |------------|---------------------------------------------|----------------------------------------------------------|
602
+ | `GET` | `/cookie-templates` | List cookie templates |
603
+ | `GET` | `/cookie-templates/:id` | Get a cookie template |
604
+ | `POST` | `/cookie-templates` | Create a cookie template |
605
+ | `PUT` | `/cookie-templates/:id` | Update a cookie template — 422 if removing cookies breaks compliance for a dependent profile |
606
+ | `DELETE` | `/cookie-templates/:id` | Delete a cookie template — 422 if any active profiles use it |
607
+ | `POST` | `/cookie-templates/:id/copy` | Duplicate a cookie template |
608
+ | `GET` | `/cookie-templates/:id/profile-usage` | List profiles using this template (`ProfileSummary[]`) |
609
+
610
+ #### Cookie Template Safety Guards
611
+
612
+ **Deletion guard:** `DELETE /cookie-templates/:id` returns `422` with `{ activeProfiles: ProfileSummary[] }` when one or more active profiles reference the template. Deactivate those profiles first, then delete.
613
+
614
+ **Cookie removal guard:** When updating a template (`PUT`), if cookies are removed, the backend checks all profiles using the template and runs compliance validation with the new cookie set. If any profile's `complianceGroup` requires a cookie that was removed, the response is `422` with:
615
+
616
+ ```json
617
+ {
618
+ "blockingProfiles": [{ "id": "...", "name": "...", "complianceGroup": "opt-in" }],
619
+ "removedCookieIds": ["analytics", "marketing"]
620
+ }
621
+ ```
622
+
623
+ Profiles listed in `blockingProfiles` must be updated (switch template or change complianceGroup) before the cookie removal can proceed.
624
+
625
+ ### UI Templates
626
+
627
+ | Method | Path | Description |
628
+ |------------|---------------------------------------------|----------------------------------------------------------|
629
+ | `GET` | `/ui-templates` | List UI templates |
630
+ | `GET` | `/ui-templates/:id` | Get a UI template |
631
+ | `POST` | `/ui-templates` | Create a UI template |
632
+ | `PUT` | `/ui-templates/:id` | Update a UI template |
633
+ | `DELETE` | `/ui-templates/:id` | Delete a UI template |
634
+ | `POST` | `/ui-templates/:id/copy` | Duplicate a UI template |
635
+ | `GET` | `/ui-templates/:id/profile-usage` | List profiles using this template (`ProfileSummary[]`) |
636
+
637
+ ### Analytics
638
+
639
+ | Method | Path | Description |
640
+ |--------|-------------------------|----------------------------------------------------|
641
+ | `GET` | `/analytics/opt-in` | Opt-in rate stats aggregated by locale and date |
642
+
643
+ Query params: `tenantId`, `profileId`, `complianceGroup`, `from` (ISO date), `to` (ISO date), `locale`.
644
+
645
+ ```json
646
+ {
647
+ "total": 12400,
648
+ "granted": 6800, "denied": 4100, "managed": 1500,
649
+ "grantedPct": 54.8, "deniedPct": 33.1, "managedPct": 12.1,
650
+ "byLocale": { "en": { "total": 8000, "granted": 4400 } },
651
+ "byDate": [{ "date": "2026-07-01", "total": 400, "granted": 220 }]
652
+ }
653
+ ```
390
654
 
391
655
  ### Consents
392
656
 
@@ -408,17 +672,37 @@ IPs are never stored raw — only SHA-256 hashes appear in the `ipHash` field.
408
672
 
409
673
  ### Users & Roles
410
674
 
411
- | Method | Path | Description |
412
- |------------|-----------------------------------|----------------------------------|
413
- | `GET` | `/users` | List admin users |
414
- | `POST` | `/users` | Create an admin user |
415
- | `PUT` | `/users/:id` | Update an admin user |
416
- | `GET` | `/roles` | List roles |
417
- | `POST` | `/roles` | Create a role |
418
- | `GET` | `/roles/:id/permissions` | Get permissions for a role |
419
- | `POST` | `/roles/:id/permissions` | Assign a permission to a role |
420
- | `DELETE` | `/roles/:id/permissions/:permId` | Revoke a permission from a role |
421
- | `GET` | `/permissions` | List all available permissions |
675
+ | Method | Path | Description |
676
+ |------------|-----------------------------------|------------------------------------------------------|
677
+ | `GET` | `/users` | List admin users |
678
+ | `POST` | `/users` | Create an admin user |
679
+ | `PUT` | `/users/:id` | Update an admin user |
680
+ | `GET` | `/roles` | List roles |
681
+ | `POST` | `/roles` | Create a role |
682
+ | `GET` | `/roles/:id/permissions` | Get permissions for a role |
683
+ | `POST` | `/roles/:id/permissions` | Assign a permission to a role |
684
+ | `DELETE` | `/roles/:id/permissions/:permId` | Revoke a permission from a role |
685
+ | `GET` | `/permissions` | List all available permissions |
686
+
687
+ #### User `allowedTenants`
688
+
689
+ Admin users can be scoped to specific tenants. When `allowedTenants` is set, the user can only see and manage data for those tenants:
690
+
691
+ ```ts
692
+ // POST /admin/users or PUT /admin/users/:id
693
+ {
694
+ "name": "APAC Manager",
695
+ "email": "apac@example.com",
696
+ "password": "...",
697
+ "allowedTenants": ["tenant-uuid-1", "tenant-uuid-2"] // empty = access to all tenants
698
+ }
699
+ ```
700
+
701
+ Users with `role: 'superadmin'` always have access to all tenants regardless of `allowedTenants`.
702
+
703
+ **Enforcement:** The auth middleware passes `allowedTenants` to every list query. A user whose `allowedTenants` does not include the tenant being queried receives an empty result set (not a 403), so pagination works normally. A user with an empty `allowedTenants` array has no restrictions. Routes for individual resources (`GET /consents/:visitorId`, `GET /consents/:visitorId/history`) return `403 Forbidden` instead of an empty result when the tenant is not in `allowedTenants`.
704
+
705
+ The **Users** dashboard page includes an **Edit** button on each user row (visible to users with `user:update` permission) that opens a modal for updating `allowedTenants` without changing the user's password or role.
422
706
 
423
707
  ### API Keys
424
708
 
@@ -430,16 +714,17 @@ IPs are never stored raw — only SHA-256 hashes appear in the `ipHash` field.
430
714
 
431
715
  ### Stats & Reporting
432
716
 
433
- | Method | Path | Description |
434
- |--------|---------------------------|------------------------------------------|
435
- | `GET` | `/stats/overview` | Total consents, visitors, GPC count |
436
- | `GET` | `/stats/timeline` | Daily consent counts (`?days=30`) |
437
- | `GET` | `/stats/categories` | Per-category grant/deny breakdown |
438
- | `GET` | `/stats/countries` | Consent count by country |
439
- | `GET` | `/stats/gpc` | GPC detection statistics |
440
- | `GET` | `/export/consents` | Export consent records (CSV or JSON) |
441
- | `GET` | `/export/consents/xlsx` | Export as XLSX |
442
- | `GET` | `/export/audit` | Export audit log (CSV or JSON) |
717
+ | Method | Path | Description |
718
+ |--------|-----------------------------------|------------------------------------------|
719
+ | `GET` | `/stats/overview` | Total consents, visitors, GPC count |
720
+ | `GET` | `/stats/timeline` | Daily consent counts (`?days=30`) |
721
+ | `GET` | `/stats/categories` | Per-category grant/deny breakdown |
722
+ | `GET` | `/stats/countries` | Consent count by country |
723
+ | `GET` | `/stats/gpc` | GPC detection statistics |
724
+ | `GET` | `/export/consents` | Export consent records (CSV or JSON) |
725
+ | `GET` | `/export/consents/xlsx` | Export as XLSX |
726
+ | `GET` | `/export/audit` | Export audit log (CSV or JSON) |
727
+ | `GET` | `/export/translations/:profileId` | Export all translatable fields as CSV |
443
728
 
444
729
  ### Audit Log
445
730
 
@@ -473,46 +758,152 @@ Only active when `tcf.enabled: true`.
473
758
 
474
759
  Served at `{basePath}/` when `dashboard: true`.
475
760
 
476
- | Section | Description |
477
- |-------------------|--------------------------------------------------------------------------|
478
- | Dashboard | Consent overview, timeline chart, country breakdown, GPC stats |
479
- | Profiles | Create / edit / copy / delete consent profiles via 3-step wizard |
480
- | Cookie Templates | Reusable cookie/purpose definitions |
481
- | UI Templates | Reusable banner + modal layout settings |
482
- | Consents | Browse, filter, export consent records; per-visitor history |
483
- | Visitors | Visitor list with geographic data |
484
- | Users | Admin user management |
485
- | Roles | RBAC roles and fine-grained permission assignment |
486
- | Sites | Multi-tenant site management |
487
- | TCF Vendors | IAB Global Vendor List |
488
- | Audit Log | Immutable log of all admin actions |
489
- | Settings | API keys, branding |
761
+ | Section | Description |
762
+ |---------------------|--------------------------------------------------------------------------|
763
+ | Dashboard | Consent overview, timeline chart, country breakdown, GPC stats |
764
+ | Profiles | Create / edit / copy / delete / activate / deactivate consent profiles |
765
+ | Profile History | Version history viewer — compare locale JSON per version |
766
+ | Cookie Templates | Reusable cookie/purpose definitions (blank by default + Load Defaults) |
767
+ | UI Templates | Reusable banner + modal layout settings |
768
+ | Consents | Browse, filter, export consent records; per-visitor history |
769
+ | Visitors | Visitor list with geographic data |
770
+ | Users | Admin user management with allowed-tenant scoping |
771
+ | Roles | RBAC roles and fine-grained permission assignment |
772
+ | Sites | Multi-tenant site management **(superadmin only)** |
773
+ | TCF Vendors | IAB Global Vendor List |
774
+ | Audit Log | Immutable log of all admin actions |
775
+ | Settings / API | API keys, branding, OpenAPI docs **(superadmin only)** |
776
+
777
+ ### Dashboard RBAC
778
+
779
+ - **Sites** and **API** sections are visible only to users with `role: 'superadmin'`
780
+ - **TCF Vendors** is visible to all authenticated users
490
781
 
491
782
  ### Profile Creation Wizard
492
783
 
493
- **Step 1 — Cookie Template:** Define cookie IDs and their properties:
784
+ **Step 1 — Profile metadata:**
785
+
786
+ | Field | Type | Description |
787
+ |---------------------|------------------------------------------------------|-----------------------------------------------------------------------------|
788
+ | `name` | `string` | Human-readable profile name |
789
+ | `defaultLocale` | `string` (BCP 47) | Locale served when visitor locale is unavailable; written as `default.json` |
790
+ | `complianceGroup` | `ComplianceGroupId` | Regulation group for geo-routing (e.g. `opt-in`, `opt-out-strict`) |
791
+ | `gpcMode` | `'ignore' \| 'honor' \| 'strict'` | GPC signal handling. Overrides group default. |
792
+ | `darkMode` | `boolean` | Enable dark mode in the consent banner |
793
+ | `hidePoweredBy` | `boolean` | Hide "Powered by Consenti" branding link |
794
+ | `allowReceipt` | `boolean` | Allow visitors to download a PDF consent receipt |
795
+ | `allowedOrigins` | `string[]` | Allowlisted domains for CORS on this profile's consent endpoints |
796
+ | `complianceConfig` | `Record<string, string>` | Per-compliance extra config (e.g. DPDPA data fiduciary name). Only shown when the selected `complianceGroup` requires it; otherwise omitted. |
797
+
798
+ **Step 2 — Cookie Template:** Define cookie IDs and their properties. Clicking **Next** at the bottom of Step 2 calls `POST /admin/profiles/validate` with the selected template's cookies and the chosen `complianceGroup`:
494
799
 
495
- | Field | Type | Description |
496
- |---------------------|-----------------------------------|---------------------------------------------------|
497
- | `id` | `string` | Unique identifier referenced in button arrays |
498
- | `type` | `'consent' \| 'legitimate_interest'` | Legal basis |
499
- | `mandatory` | `boolean` | Always granted; toggle disabled in modal |
500
- | `listenGpc` | `boolean` | Auto-denied when GPC signal is active |
501
- | `expiry` | `number` | Days until re-consent is required (0 = session) |
800
+ - **Compliance errors** (red panel): block advancing to Step 3 until resolved (e.g. template has no cookie with `listenGpc: true` but the group requires GPC support).
801
+ - **Compliance warnings** (amber panel): show an acknowledgment checkbox; the user must check it before advancing. Warnings do not block save — they surface potential issues.
502
802
 
503
- **Step 2 — UI Template:** Configure banner and modal layout:
803
+ | Field | Type | Description |
804
+ |---------------------|-------------------------------------------------|---------------------------------------------------------------------------|
805
+ | `id` | `string` | Unique identifier referenced in button arrays |
806
+ | `legalBasis` | `'mandatory' \| 'consent' \| 'legitimate_interest'` | Legal basis (replaces deprecated `mandatory`+`type` fields) |
807
+ | `listenGpc` | `boolean` | Auto-denied when GPC signal is active |
808
+ | `expiry` | `number` | Days until re-consent is required (0 = session) |
809
+ | `tcfSpecialFeatures`| `number[]` | IAB TCF special feature IDs (SF1, SF2) associated with this cookie |
810
+
811
+ **Step 3 — UI Template:** Configure banner and modal layout. New UI templates start **blank** (no prefilled buttons or categories). Click **Load Defaults** in the amber callout to populate a sensible starter structure.
504
812
 
505
813
  - **Main Banner / GPC Banner:** `position`, `overlayOpacity`, `showClose`, `headingTag`, `buttons`
506
814
  - **Preference Modal:** `position`, `overlayOpacity`, `showClose`, `persistent`, `buttons`, `categories`
507
815
  - Button `type`: `primary | secondary | text | reject | submit | manage | close`
508
816
  - Button `cookies`: `*` (grant all) · `!` (deny all) · comma-separated IDs
509
817
 
510
- **Step 3 — Content:** Enter localised copy with live preview:
818
+ **Step 4 — Content:** Enter localised copy with live preview:
511
819
 
512
820
  - Main Banner: heading, body HTML
513
821
  - GPC Banner: heading, body HTML
514
822
  - Preference Modal: heading, subheading, intro HTML, per-category heading + description
515
823
 
824
+ **Adding locales:** Click **+ Add locale** to open a searchable `CountrySelecter` dropdown showing all BCP 47 locale options. Select a locale to add its tab instantly.
825
+
826
+ **Default-locale placeholders:** When editing a non-default locale tab, all heading, subheading, and category-heading inputs show the default locale's value as placeholder text — so translators see the source copy without switching tabs.
827
+
828
+ **Import / Export:**
829
+
830
+ | Button | Action |
831
+ |--------|--------|
832
+ | **JSON** | Exports *all* locale tabs as a single `locales.json` file (`{ "en": {...}, "fr-FR": {...} }`) |
833
+ | **CSV** | Exports the *current* locale tab as a `locale-{locale}.csv` file |
834
+ | **Import** | Accepts either format — multi-locale JSON auto-creates missing locale tabs; skipped locales (invalid BCP 47 key) are listed in an amber callout after import |
835
+
836
+ **Readability callouts:** Inline advisory warnings (yellow) appear beneath `heading` and body HTML fields when:
837
+ - A heading exceeds 80 characters
838
+ - Average sentence length in the body exceeds 25 words
839
+ - Total word count in the body exceeds 150 words
840
+
841
+ These are informational only — the profile can still be saved.
842
+
843
+ ### Profile Activation
844
+
845
+ Profiles are **inactive** after creation. To serve them via geo-routing:
846
+
847
+ 1. Click **Activate** in the profile list or use `POST /admin/profiles/:id/activate`
848
+ 2. Consenti writes locale JSON files to `${storage.path}/profiles/${tenantId}/${complianceGroup}/`
849
+ 3. The static file route starts serving these files immediately (zero DB on the hot path)
850
+
851
+ Only one profile per `complianceGroup` can be active at a time. Activating a new profile automatically deactivates the existing one.
852
+
853
+ ### Profile Version History
854
+
855
+ Every profile save creates an immutable snapshot at `${profileId}/${version}/`. The dashboard version history page (`#/banners/profiles/:id/history`) shows:
856
+ - Left panel: list of all versions with dates
857
+ - Right panel: prettified JSON for the selected version; locale switcher dropdown
858
+
859
+ Via API:
860
+ ```http
861
+ GET /consenti/admin/profiles/:id/versions
862
+ GET /consenti/admin/profiles/:id/versions/3?locale=fr-FR
863
+ ```
864
+
865
+ ### Template Save Safety
866
+
867
+ When saving a **Cookie Template** or **UI Template** that is used by one or more profiles, a confirmation dialog appears listing the affected profiles. You must confirm before changes are applied. Uses `GET /admin/cookie-templates/:id/profile-usage` or `GET /admin/ui-templates/:id/profile-usage` internally.
868
+
869
+ When **deleting** a Cookie Template used by active profiles, the delete is blocked with a 422 error listing those profiles. Deactivate the affected profiles before deleting the template.
870
+
871
+ When **removing cookies** from a Cookie Template, the backend checks whether the reduced cookie set satisfies the `complianceGroup` rules for every profile using the template. If any profile's compliance would break, the save is blocked with a 422 listing the `blockingProfiles` and `removedCookieIds` — see [Cookie Template Safety Guards](#cookie-template-safety-guards).
872
+
873
+ ---
874
+
875
+ ## EventBus
876
+
877
+ `createConsenti()` returns an `eventBus` — a standard Node.js `EventEmitter` — that fires events at every data lifecycle step.
878
+
879
+ | Event | Payload | Description |
880
+ |---|---|---|
881
+ | `ready` | none | Storage connected and admin user bootstrapped |
882
+ | `consent.created` | `ConsentDbRecord` | New consent record saved |
883
+ | `consent.updated` | `{ previous, current: ConsentDbRecord }` | Consent record updated |
884
+ | `consent.erased` | `{ visitorId: string }` | Visitor data erased (GDPR erasure) |
885
+ | `visitor.created` | `Visitor` | New visitor record created |
886
+ | `profile.created` | `Profile` | Profile created |
887
+ | `profile.updated` | `{ previous, current: Profile }` | Profile updated (version bumped) |
888
+ | `profile.deleted` | `{ id: string, previous: Profile }` | Profile deleted |
889
+ | `cache:warm` | `{ paths: string[], version: number }` | Fired after locale JSON files are written; paths to warm in CDN/cache |
890
+ | `cache:purge` | `{ paths: string[], version: number }` | Fired after locale JSON files are removed; paths to purge from CDN/cache |
891
+
892
+ `cache:warm` and `cache:purge` mirror the `handleCache` config callback — use whichever suits your integration.
893
+
894
+ ```ts
895
+ const { eventBus } = createConsenti({ /* ... */ })
896
+
897
+ eventBus.on('consent.created', (record) => {
898
+ // send to analytics, trigger webhook, etc.
899
+ console.log('New consent from visitor:', record.visitorId)
900
+ })
901
+
902
+ eventBus.on('consent.erased', ({ visitorId }) => {
903
+ myDmpClient.deleteVisitor(visitorId)
904
+ })
905
+ ```
906
+
516
907
  ---
517
908
 
518
909
  ## Plugins (Server-side)
@@ -595,13 +986,26 @@ await consenti.destroy()
595
986
 
596
987
  ## Environment Variables
597
988
 
598
- | Variable | Description |
599
- |---------------------------|-----------------------------------------------------------------------------|
600
- | `NODE_ENV` | Set to `'production'` to suppress stack traces in error responses |
601
- | `CONSENTI_ADMIN_PASSWORD` | Override `auth.adminPassword` from config |
602
- | `CONSENTI_ADMIN_JWT_SECRET` | Override `auth.jwtSecret` from config |
603
- | `CONSENTI_DB_PATH` | Override `storage.path` (SQLite) |
604
- | `CONSENTI_BASE_PATH` | Override `basePath` |
989
+ All config fields can be set via environment variables. Code config takes precedence over env vars.
990
+
991
+ | Variable | Config field | Default |
992
+ |-------------------------------------|------------------------------------|----------------------------------|
993
+ | `NODE_ENV` | — | Suppress stack traces when `'production'` |
994
+ | `CONSENTI_BASE_PATH` | `basePath` | `'/consenti'` |
995
+ | `CONSENTI_ADMIN_EMAIL` | `auth.adminEmail` | `'user@consenti.dev'` |
996
+ | `CONSENTI_ADMIN_PASSWORD` | `auth.adminPassword` | `'Consenti@123'` |
997
+ | `CONSENTI_ADMIN_JWT_SECRET` | `auth.jwtSecret` | random per restart |
998
+ | `CONSENTI_DB_DRIVER` | `storage.driver` | `'json'` |
999
+ | `CONSENTI_DB_PATH` | `storage.path` | `'./consenti-data'` |
1000
+ | `CONSENTI_DB_URI` | `storage.uri` | — |
1001
+ | `CONSENTI_DB_DATABASE` | `storage.database` | — |
1002
+ | `CONSENTI_DB_HOST` | `storage.host` | — |
1003
+ | `CONSENTI_DB_PORT` | `storage.port` | — |
1004
+ | `CONSENTI_DB_USER` | `storage.user` | — |
1005
+ | `CONSENTI_DB_PASSWORD` | `storage.password` | — |
1006
+ | `CONSENTI_RATE_LIMIT_WINDOW_MS` | `rateLimit.windowMs` | `60000` |
1007
+ | `CONSENTI_RATE_LIMIT_MAX_REQUESTS` | `rateLimit.maxRequests` | `60` |
1008
+ | `CONSENTI_MAX_BODY_SIZE` | `maxBodySize` | `1048576` (1 MB) |
605
1009
 
606
1010
  ---
607
1011
 
@@ -610,6 +1014,15 @@ await consenti.destroy()
610
1014
  When running, Swagger UI is available at `{basePath}/api/docs`.
611
1015
  OpenAPI JSON at `{basePath}/api/openapi.json`.
612
1016
 
1017
+
1018
+ ---
1019
+
1020
+ ## Demo
1021
+ For detailed API docs, Guides and Demo, visit [consenti.dev](https://consenti.dev)
1022
+
1023
+ Visit author's profile at [santosh.top](https://santosh.top)
1024
+
1025
+
613
1026
  ---
614
1027
 
615
1028
  ## Contributing