Backend — Configuration
createConsenti(config) accepts a single configuration object. Every field is optional — calling it with an empty object is valid and gives you a running server immediately.
Minimal config to get started
This is all you need. Copy it, set your password via an env var, and you have a working server with admin dashboard.
import { createConsenti } from '@consenti/api'
import http from 'node:http'
const consenti = createConsenti({
storage: { driver: 'json', path: './consenti-data' },
auth: {
mode: 'local',
adminEmail: '[email protected]',
adminPassword: process.env.CONSENTI_ADMIN_PASSWORD!,
},
dashboard: true,
})
http.createServer(consenti.handler).listen(3001)
// Admin → http://localhost:3001/consenti/
// API → http://localhost:3001/consenti/api/v1/json driver stores data in memory and flushes to disk. It is fine for development, but switch to SQLite or a server database before production. See storage below.Full configuration reference
Every available option shown with its default value.
import { createConsenti } from '@consenti/api'
const consenti = createConsenti({
// Optional but recommended
storage: {
driver: 'json', // not recommended for production
path: './consenti-data', // directory path — Consenti creates db/, profiles/, logs/ inside
},
auth: {
mode: 'local',
adminEmail: process.env.CONSENTI_ADMIN_EMAIL ?? '[email protected]',
adminPassword: process.env.CONSENTI_ADMIN_PASSWORD ?? 'Consenti@123',
jwtSecret: process.env.CONSENTI_ADMIN_JWT_SECRET ?? 'random-jwt-secret',
},
// Optional
basePath: '/consenti', // URL prefix for all routes
dashboard: true, // serve admin SPA at basePath
rateLimit: {
windowMs: 60_000, // 1-minute window
maxRequests: 60, // requests per window per IP
},
compliance: {
type: 'auto', // 'auto' = geo-resolve per visitor | ComplianceGroupId = fixed group
geoDataProvider: 'default', // 'default' | 'hosted-geoip-lite' | 'geoip' | 'maxmind' | CountryResolverFn
autoComplianceMap: 'default', // 'default' (embedded 240-country map) | object | 'auto' (remote URL)
},
// S3 sync for profile locale JSON files (optional)
s3Api: {
enabled: false,
region: 'us-east-1',
bucketName: 'consenti-profiles',
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
// sessionToken: process.env.AWS_SESSION_TOKEN, // for temporary credentials
},
// CDN / cache invalidation hook (optional)
handleCache: (paths, version, isPurge) => {
// paths: locale file paths written or removed
// isPurge: true = files removed (deactivate/delete); false = files written (create/activate)
},
tcf: {
enabled: false,
cmpId: 0,
cmpVersion: 1,
},
ageGate: {
enabled: false,
minimumAge: 13,
requireParentalConsent: false,
},
dataRetention: {
purgeAfterDays: 365, // delete consent records older than this
},
multiTenant: {
enabled: false,
},
maxBodySize: 1_048_576, // 1 MB request body limit
trustedProxies: [], // IPs / CIDRs of reverse proxies (nginx, Cloudflare…)
branding: {
appName: 'My CMP', // dashboard header, login page, browser tab
appLogoPath: './logo.svg', // local file or https:// URL
hidePoweredBy: false, // true = hide "Powered by Consenti" badge
},
plugins: [],
})storage
Controls which database backend Consenti uses. The driver field selects the engine; the remaining fields depend on which driver you choose.
storage.path is a directory
storage.path is a directory path, not a file path. Consenti creates the following layout inside it automatically:
${storage.path}/
db/
consenti-data.json ← JSON adapter
consenti.db ← SQLite adapters
profiles/
${tenantId}/
${profileId}/
1/ ← version 1 (immutable once written)
en.json
fr-FR.json
default.json ← copy of defaultLocale content
2/ ← version 2 on next edit
${complianceGroup}/ ← hot-serve path (active profile only)
en.json
fr-FR.json
default.json
logs/ ← reserved for future usestorage.path has a file extension (.json, .db), the parent directory is used and a deprecation warning is logged.Default driver — json
When no storage config is provided, Consenti uses the built-in JSON file adapter. It requires zero installation — only node:fs and node:crypto, both Node.js built-ins. Data is kept in memory and written to disk atomically after every mutation (temp-file + rename, same guarantee as steno).
storage: { driver: 'json', path: './consenti-data' } // path is a directoryjson driver loads the entire dataset into memory and is single-process only. It is designed for development, prototyping, and low-traffic single-instance deployments. Use a SQLite or server driver in production.SQLite drivers
Three SQLite drivers are available. All create the same schema; choose based on your Node.js version and whether you can compile native addons.
| Driver | Requires | Notes |
|---|---|---|
node:sqlite | Node 22.5+ | Built-in, zero compilation. Recommended for Node 22.5+. |
better-sqlite3 / sqlite | Node 20+, native addon | Fastest option. Requires a prebuilt binary or build toolchain. sqlite is an alias. |
node-sqlite3-wasm | Node 20+ | WASM bundle, no compilation. Use when node:sqlite is unavailable and native addons are blocked. |
node:sqlite is built into Node 22.5+ and requires no installation. The other two drivers are optional peer dependencies — install whichever one you need:
# Node 20+ native addon (fastest; needs build toolchain or prebuilt binary)
npm install better-sqlite3
# Node 20+ WASM fallback (no compilation required)
npm install node-sqlite3-wasm// Node 22.5+ — built-in, no install needed
storage: { driver: 'node:sqlite', path: './consenti.db' }
// Node 20+ with native addon
storage: { driver: 'better-sqlite3', path: './consenti.db' }
// Node 20+ WASM fallback
storage: { driver: 'node-sqlite3-wasm', path: './consenti.db' }Server drivers
| Driver | Connection field | Notes |
|---|---|---|
postgresql | uri | PostgreSQL 13+. Connection pool via pool. |
mysql | uri | MySQL 8+ / MariaDB 10.6+. Connection pool via pool. |
mongodb | uri | MongoDB 6+. Optional database override. |
Each server driver is an optional peer dependency — install the one you need:
npm install pg # PostgreSQL
npm install mysql2 # MySQL / MariaDB
npm install mongodb # MongoDB// PostgreSQL
storage: { driver: 'postgresql', uri: process.env.CONSENTI_DATABASE_URL }
// MySQL
storage: { driver: 'mysql', uri: process.env.CONSENTI_DATABASE_URL }
// MongoDB
storage: { driver: 'mongodb', uri: process.env.CONSENTI_DATABASE_URL, database: 'consenti' }storage field reference
| Key | Type | Default | Description |
|---|---|---|---|
driver | string | 'json' (when storage omitted) | Required within the storage object. Selects the storage engine (see tables above). |
path | string | './consenti-data' | Directory path for local drivers (json, SQLite). Consenti creates db/, profiles/, and logs/ subdirectories inside. Relative paths resolve from process.cwd(). Must not be inside the package installation directory. |
uri | string | — | Server drivers only. Full connection string including credentials. |
database | string | name in uri | MongoDB only. Database name override. |
host / port | string / number | — | Alternative to uri for server drivers. |
user / password | string | — | Credentials when not embedded in uri. |
database | string | — | Database name for PostgreSQL / MySQL when not in uri. |
auth
The auth object controls how the admin dashboard authenticates its users. Pick one mode and supply the fields that mode requires.
When auth is omitted entirely, Consenti defaults to mode: 'local' and reads credentials from environment variables:
| Env var | Default fallback | Purpose |
|---|---|---|
CONSENTI_ADMIN_EMAIL | [email protected] | Bootstrap super-admin email when no auth.adminEmail is set. |
CONSENTI_ADMIN_PASSWORD | Consenti@123 | Bootstrap super-admin password when no auth.adminPassword is set. |
CONSENTI_ADMIN_JWT_SECRET | random per restart | JWT signing secret. Takes precedence over auth.jwtSecret. |
CONSENTI_ADMIN_JWT_SECRET means all sessions are invalidated on restart. Always set a stable secret in production.Shared options (all modes)
| Key | Type | Default | Description |
|---|---|---|---|
mode | 'local' | 'jwt' | 'oidc' | 'saml' | 'custom' | 'local' | Authentication strategy for the admin dashboard. |
jwtSecret | string | random on start | HMAC-SHA256 secret used to sign session JWTs. Omit in dev; set a stable value in production or sessions expire on restart. |
mode: 'local'
Built-in email + password login. On first boot, Consenti creates a super-admin account using adminEmail / adminPassword. The password is hashed with scrypt (node:crypto) and never stored in plain text. Subsequent logins issue a signed JWT stored in an HttpOnly cookie.
Brute-force protection is built in: 5 failed attempts within 15 minutes lock the account for the remainder of that window.
auth: {
mode: 'local',
adminEmail: '[email protected]',
adminPassword: process.env.CONSENTI_ADMIN_PASSWORD!, // min 12 chars; 16+ recommended
jwtSecret: process.env.CONSENTI_ADMIN_JWT_SECRET!, // stable secret for production
}| Key | Required | Default | Description |
|---|---|---|---|
adminEmail | yes | env.CONSENTI_ADMIN_EMAIL → '[email protected]' | Email address for the bootstrapped super-admin user. |
adminPassword | yes | env.CONSENTI_ADMIN_PASSWORD → 'Consenti@123' | Password for the bootstrapped super-admin user. Must be ≥ 12 characters; ≥ 16 recommended. Hashed with scrypt on first boot; ignored on subsequent starts. |
mode: 'jwt'
Validate JWTs issued by an external system (e.g. your own auth service). Consenti verifies the token signature with jwtSecret and extracts sub, email, roles, and tenantIdfrom the payload.
auth: {
mode: 'jwt',
jwtSecret: process.env.CONSENTI_ADMIN_JWT_SECRET!, // must match the secret used to sign your tokens
}Your auth service must issue tokens with at least these claims:
{
"sub": "user-id",
"email": "[email protected]",
"roles": ["super_admin"],
"tenantId": "default"
}alg: "HS256") tokens are supported in this mode. Tokens signed with RS256 / ES256 require mode: 'oidc'.mode: 'oidc'
OpenID Connect Authorization Code flow with PKCE. Consenti auto-discovers endpoints from the issuer's /.well-known/openid-configurationand verifies the ID token signature using the provider's JWKS. Supports RS256, RS384, RS512, ES256, ES384, and ES512.
auth: {
mode: 'oidc',
jwtSecret: process.env.CONSENTI_ADMIN_JWT_SECRET!, // signs the session cookie after OIDC login
oidc: {
issuer: 'https://accounts.google.com', // or any OIDC-compliant provider
clientId: process.env.OIDC_CLIENT_ID!,
clientSecret: process.env.OIDC_CLIENT_SECRET!,
redirectUri: 'https://yourapp.com/consenti/auth/oidc/callback',
claimsMapping: {
email: 'email', // default; override if your IdP uses a different claim
roles: 'consenti_roles', // default; the claim that carries Consenti role names
},
},
}| Key | Required | Default | Description |
|---|---|---|---|
oidc.issuer | yes | — | OIDC provider base URL. Must expose /.well-known/openid-configuration. |
oidc.clientId | yes | — | OAuth 2.0 client ID registered with the provider. |
oidc.clientSecret | yes | — | OAuth 2.0 client secret. |
oidc.redirectUri | yes | — | Callback URL registered with the provider. Must be: {basePath}/auth/oidc/callback. |
oidc.claimsMapping.email | no | "email" | JWT claim that holds the user's email. |
oidc.claimsMapping.roles | no | "consenti_roles" | JWT claim that holds an array of Consenti role names. |
state / verifier pairs are held in memory. For multi-instance deployments, replace the in-memory store with a shared cache (Redis, etc.) by extending the auth handler.mode: 'saml'
SAML 2.0 SP-initiated SSO. Consenti acts as the Service Provider; your Identity Provider handles authentication. Assertion validation is delegated to the samlify peer dependency, which you must install separately.
npm install samlifyauth: {
mode: 'saml',
jwtSecret: process.env.CONSENTI_ADMIN_JWT_SECRET!,
saml: {
issuer: 'https://idp.example.com',
entryPoint: 'https://idp.example.com/sso/saml', // IdP SSO URL
cert: process.env.IDP_CERT!, // IdP signing certificate (PEM, without headers)
callbackUrl: 'https://yourapp.com/consenti/auth/saml/callback',
},
}| Key | Required | Description |
|---|---|---|
saml.issuer | yes | IdP entity ID / issuer string. |
saml.entryPoint | yes | IdP SSO endpoint URL (HTTP-Redirect binding). |
saml.cert | yes | IdP X.509 signing certificate in PEM format (without the -----BEGIN CERTIFICATE----- headers). |
saml.callbackUrl | yes | Your ACS (Assertion Consumer Service) URL. Must be: {basePath}/auth/saml/callback. |
Consenti exposes an SP metadata endpoint at {basePath}/auth/saml/metadata. Register this URL with your IdP or download the XML to configure the SP manually.
The SAML assertion must contain a NameID in email format and an optional roles attribute holding an array of Consenti role names.
mode: 'custom'
Bring your own authentication logic. Supply a validateUser async function that receives the raw Request object and returns an AdminUser(or null if the request is unauthenticated). Consenti calls this function on every protected admin route.
import type { AdminUser } from '@consenti/types'
auth: {
mode: 'custom',
validateUser: async (req: Request): Promise<AdminUser | null> => {
const token = req.headers.get('authorization')?.replace('Bearer ', '')
if (!token) return null
// call your own auth service, validate a session cookie, etc.
const user = await myAuthService.verifyToken(token)
if (!user) return null
return {
id: user.id,
tenantId: 'default',
name: user.name,
email: user.email,
passwordHash: '',
isActive: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
},
}custommode you own the entire authentication step. Consenti still enforces RBAC on the returned user's roles — the custom handler only decideswho the user is, not what they are allowed to do.TOTP / Two-factor authentication
TOTP (RFC 6238) is available as an optional second factor in local mode. It is implemented entirely with node:crypto — no external dependency.
import {
generateTotpSecret,
totpQrUrl,
verifyTotp,
} from '@consenti/api'
// 1. Generate and persist a secret per user
const secret = generateTotpSecret() // base32 string
// 2. Show the QR code to the user during setup
const otpauthUrl = totpQrUrl(secret, '[email protected]', 'MyApp')
// → otpauth://totp/MyApp:admin%40example.com?secret=...
// 3. Verify a submitted 6-digit code (window = ±1 time-step)
const ok = verifyTotp(secret, submittedCode)verifyTotp rejects a token that has already been accepted, preventing replay attacks within the same 30-second window.basePath
URL prefix for all Consenti routes. Defaults to /consenti.
| Key | Type | Default | Description |
|---|---|---|---|
basePath | string | '/consenti' | All public API, admin API, and dashboard routes are mounted under this prefix. |
// Default — routes at /consenti/api/v1/...
basePath: '/consenti'
// Custom prefix
basePath: '/cmp'
// → public API: /cmp/api/v1/consent
// → admin API: /cmp/admin/...
// → dashboard: /cmp (SPA)
// → OpenAPI UI: /cmp/api/docsdashboard
Controls whether Consenti serves the built-in Preact admin SPA. Accepts a boolean or a config object.
| Value | Effect |
|---|---|
true | Serve the dashboard at {basePath}. |
false / omitted | Dashboard files are not served; admin API routes remain active. |
{ enabled: true, path: '/admin' } | Serve at a custom sub-path instead of basePath. |
// Simplest — serve at /consenti
dashboard: true
// Disable (headless / API-only mode)
dashboard: false
// Serve at a custom path
dashboard: { enabled: true, path: '/admin' }@consenti/api package directory.rateLimit
In-memory sliding-window rate limiter applied to public API routes (consent reads / writes). Admin routes use a separate, fixed-window limiter (10 requests / 15 min) that cannot be configured.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Set to false to disable rate limiting entirely (e.g. behind your own proxy that already rate-limits). |
windowMs | number | 60000 | Rolling window duration in milliseconds. |
maxRequests | number | 60 | Maximum requests per IP per window. Requests over the limit receive 429 Too Many Requests. |
// Tighter limit for high-traffic sites
rateLimit: {
windowMs: 60_000, // 1-minute window
maxRequests: 30, // 30 req/min per IP
}
// Disable entirely (you handle rate limiting upstream)
rateLimit: { enabled: false }compliance
Controls which cookie consent model is applied to visitors. The recommended approach is automatic geo-routing (type: 'auto') — Consenti resolves the visitor's jurisdiction from geo signals and selects the correct compliance group.
Compliance groups
All behaviour (opt-in vs opt-out, GPC handling, LI validity) is determined by the visitor's compliance group. Eight groups are built in:
| Group ID | Model | Key regulations |
|---|---|---|
opt-in | Opt-in (GDPR) | GDPR, UK-GDPR, nFADP, KVKK, PDPA-TH, Middle East laws |
opt-out | Opt-out (US state laws) | CCPA, VCDPA, CPA-CO, CTDPA, UCPA, 15 other US state laws |
opt-out-strict | Opt-out strict (CPRA) | CPRA / California — GPC mandatory, sale/sharing categories |
opt-in-dpdpa | Opt-in (India) | DPDPA — no Legitimate Interest, data fiduciary disclosure |
opt-in-china | Opt-in (China) | PIPL, DSL, CSL — strict opt-in, no LI |
opt-in-brazil | Opt-in (Brazil) | LGPD — opt-in with LI allowed under impact assessment |
general-privacy-consent | General consent | PIPEDA, POPIA, APPI, PDPA-SG/MY, 40+ others |
notice-only | Notice only | Jurisdictions with notice requirements but no consent mandate |
compliance field reference
| Key | Type | Default | Description |
|---|---|---|---|
type | 'auto' | ComplianceGroupId | 'auto' | 'auto' geo-resolves per visitor; a fixed ComplianceGroupId applies one group to every visitor globally. |
geoDataProvider | 'default' | 'hosted-geoip-lite' | 'geoip' | 'maxmind' | CountryResolverFn | 'default' | Source used to resolve the visitor's country. 'default' — timezone + Accept-Language heuristic, zero deps, least accurate. 'hosted-geoip-lite' — calls ipinfo.io via node:https, no install, requires outbound internet. 'geoip' — local lookup via optional geoip-lite peer dep. 'maxmind' — official MaxMind SDK, most accurate, requires .mmdb file. Pass a CountryResolverFn to use any custom source — must return { country: string | null, region: string | null, locale: string | null }. |
autoComplianceMap | 'default' | ComplianceMapData | 'auto' | 'default' | 'default' uses the embedded 240-country map bundled with the package. Pass a ComplianceMapData object to supply your own.'auto' fetches from complianceMapUrl at startup and refreshes every 24 h. |
complianceMapUrl | string | — | Remote URL for the compliance map JSON. Only used when autoComplianceMap: 'auto'. |
ignore / honor / strict) is a per-profile setting configured in the dashboard profile wizard (gpcMode) — not a global compliance config option. The compliance group provides a default GPC behaviour; the profile can override it.// Zero-dep geo routing (timezone + Accept-Language heuristic)
compliance: {
type: 'auto',
geoDataProvider: 'default',
autoComplianceMap: 'default',
}
// IP-based — no install, calls ipinfo.io (requires outbound internet)
compliance: {
type: 'auto',
geoDataProvider: 'hosted-geoip-lite',
}
// IP-based — local lookup via geoip-lite (npm install geoip-lite)
compliance: {
type: 'auto',
geoDataProvider: 'geoip',
}
// Fixed group — GDPR-only product serving only EU visitors
compliance: {
type: 'opt-in',
}
// Custom geo resolver — integrate your own GeoIP SaaS
compliance: {
type: 'auto',
geoDataProvider: async ({ ip, timezone, language }) => {
const res = await fetch(`https://api.mygeoip.com/${ip}`)
const data = await res.json()
return { country: data.country_code, region: data.region, locale: data.locale ?? null }
},
}ComplianceMapData object or point complianceMapUrl at a hosted JSON file you control.s3Api
When enabled, Consenti uploads every locale JSON file to S3 after writing it to disk. The /resolve-profile endpoint then returns an S3 URL so the widget can fetch the profile directly from your CDN — zero server round-trips on the hot path.
S3 signing is implemented with node:crypto and fetch — no AWS SDK dependency.
| Key | Required | Description |
|---|---|---|
enabled | yes | Set to true to activate S3 sync. |
region | yes | AWS region, e.g. 'us-east-1'. |
bucketName | yes | S3 bucket name. Must allow PutObject on profiles/*. |
accessKeyId | yes | AWS access key ID. |
secretAccessKey | yes | AWS secret access key. |
sessionToken | no | AWS session token. Required only for temporary credentials (STS assume-role). |
s3Api: {
enabled: true,
region: 'us-east-1',
bucketName: 'my-cmp-profiles',
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
}handleCache
A callback invoked whenever Consenti writes or removes locale JSON files on disk. Use it to integrate with a CDN, nginx proxy cache, or any reverse proxy that caches the hot-serve profile files.
This is the config-based alternative to listening on the cache:warm and cache:purge EventBus events — use whichever fits your integration style.
| Argument | Type | Description |
|---|---|---|
paths | string[] | Absolute file paths written or removed in this operation. |
version | number | Current profile version at the time of the operation. |
isPurge | boolean | true — files were removed (deactivate / delete / update); false — files were written (create / activate). |
handleCache: (paths, version, isPurge) => {
if (isPurge) {
// Purge these paths from your CDN / nginx proxy cache
for (const p of paths) {
cdnClient.purge(p)
}
} else {
// Warm the cache at these paths
for (const p of paths) {
cdnClient.warm(p)
}
}
}tcf
IAB Transparency & Consent Framework v2.2 integration. When enabled, Consenti fetches the Global Vendor List, stores TC strings on consent records, and exposes the __tcfapi stub endpoint.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false (omit tcf to disable) | Enable TCF mode. Starts the GVL refresh background job on boot. |
cmpId | number | — | Your IAB-registered CMP ID. Required when enabled: true. |
cmpVersion | number | — | Your CMP software version number. Required when enabled: true. Included in TC strings. |
tcf: {
enabled: true,
cmpId: 42, // your IAB CMP ID
cmpVersion: 1,
}ageGate
Adds an age-verification step before consent can be recorded. When enabled, the consent submission endpoint rejects records that do not carry a valid age attestation.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable age gating. |
minimumAge | number | 13 | Minimum age required to record consent. Common values: 13 (COPPA), 16 (GDPR Article 8 default). |
requireParentalConsent | boolean | false | When true, users below minimumAge must provide a parental consent token instead of being rejected outright. |
// COPPA — block users under 13
ageGate: {
enabled: true,
minimumAge: 13,
}
// GDPR Article 8 — require parental consent for users 13–15
ageGate: {
enabled: true,
minimumAge: 16,
requireParentalConsent: true,
}dataRetention
Automatic GDPR data minimisation. When configured, Consenti runs a nightly background job that hard-deletes consent records older than purgeAfterDays.
| Key | Type | Default | Description |
|---|---|---|---|
purgeAfterDays | number | — (omit to disable) | Delete consent records older than this many days. The purge runs once at startup and then every 24 hours. |
// Keep consent records for 1 year, then purge automatically
dataRetention: {
purgeAfterDays: 365,
}audit_logs) are never purged — they are append-only by design. Only consent records are affected by this setting.multiTenant
When enabled, Consenti resolves the tenant from the request and scopes all data operations to that tenant. The tenant identifier is extracted from the X-Tenant-ID header or the tenantId query parameter.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable multi-tenant mode. All requests without a valid tenant ID fall back to "default". |
multiTenant: { enabled: true }
// Clients must then pass:
// X-Tenant-ID: acme-corp
// or: GET /consenti/api/v1/consent?tenantId=acme-corpmaxBodySize
Maximum allowed request body size in bytes. Requests exceeding this limit receive a 413 Payload Too Large response before any parsing occurs.
| Key | Type | Default | Description |
|---|---|---|---|
maxBodySize | number | 1048576 (1 MB) | Body size limit in bytes. |
maxBodySize: 512_000 // 512 KBtrustedProxies
List of IP addresses or CIDR ranges for reverse proxies that sit in front of Consenti (nginx, Cloudflare, AWS ALB, etc.). When set, Consenti reads the real client IP from the X-Forwarded-For header instead of the TCP connection address. This ensures rate limiting and IP hashing target the actual visitor, not the proxy.
| Key | Type | Default | Description |
|---|---|---|---|
trustedProxies | string[] | [] | IP addresses or CIDR blocks of trusted reverse proxies. |
// Single proxy
trustedProxies: ['127.0.0.1']
// Private network (e.g. Kubernetes cluster)
trustedProxies: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']X-Forwarded-For from an untrusted source could bypass rate limiting.plugins
Consenti ships a plugin system that lets you hook into the data lifecycle without modifying the package. Plugins are class instances that extend ConsentiServerPlugin.
import {
ConsentiServerPlugin,
type PluginContext,
type CreateConsentInput,
type ConsentDbRecord,
} from '@consenti/api'
export class AuditWebhookPlugin extends ConsentiServerPlugin {
name = 'audit-webhook'
async initialize(ctx: PluginContext): Promise<void> {
// ctx.storage — direct StorageAdapter access
// ctx.config — full ConsentiServerConfig
console.log('AuditWebhookPlugin ready')
}
async afterConsentSave(record: ConsentDbRecord): Promise<void> {
await fetch('https://webhook.example.com/consent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(record),
})
}
async destroy(): Promise<void> {
// clean up connections, timers, etc.
}
}
// Register it:
plugins: [new AuditWebhookPlugin()]Available hooks
| Hook | When it fires | Can mutate? |
|---|---|---|
initialize(ctx) | After storage connects, before first request. | — |
destroy() | On server shutdown. | — |
beforeConsentSave(data) | Before a new consent record is written. | yes — return modified data |
afterConsentSave(record) | After a new consent record is persisted. | no |
beforeConsentUpdate(data) | Before an existing consent record is updated. | yes — return modified data |
afterConsentUpdate(record) | After an existing consent record is updated. | no |
beforeProfileFetch(id) | Before a profile is loaded by ID. | yes — return a different id |
afterProfileFetch(profile) | After a profile is loaded. | yes — return modified profile |
beforeUserCreate(data) | Before a new admin user is created. | yes — return modified data |
afterUserCreate(user) | After a new admin user is persisted. | no |
branding
Customises the appearance of the admin dashboard — login page, sidebar, and browser tab title. All fields are optional; defaults give you a standard Consenti-branded experience.
| Key | Type | Default | Description |
|---|---|---|---|
appName | string | 'Consenti' | Product name shown in the dashboard header, login page, and browser tab. |
appLogoPath | string | Consenti default logo | Path to your logo file or a public URL (https://…). When a local file path is given, the file is automatically copied into the dashboard bundle and served as a static asset — no extra route needed. |
hidePoweredBy | boolean | false | When true, removes the "Powered by Consenti" badge from the dashboard footer. |
// Minimal — just change the app name
branding: {
appName: 'Acme CMP',
}
// Full white-label — custom name, logo, and hide badge
branding: {
appName: 'Acme CMP',
appLogoPath: './assets/acme-logo.svg', // local file — copied and served automatically
hidePoweredBy: true,
}
// Logo from a public CDN
branding: {
appName: 'Acme CMP',
appLogoPath: 'https://cdn.acme.com/logo.svg',
}Security rules (built-in)
- Raw IP addresses are never stored — SHA-256 hashed:
createHash('sha256').update(ip).digest('hex') - Passwords hashed with scrypt via
node:crypto— not bcrypt (external dep) - JWT signed with
node:cryptoHMAC-SHA256 — not jsonwebtoken package - Admin routes always go through
auth.middleware.ts - Security headers (
X-Content-Type-Options,X-Frame-Options,Referrer-Policy) added to every response - Stack traces hidden in production (
NODE_ENV === 'production')