Consenti

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.

server.ts
ts
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/
⚠️The 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.
ℹ️The rest of this page is the complete configuration reference. You don't need to read it all now — come back when you need to change a specific option like the database driver, auth mode, or compliance settings.

Full configuration reference

Every available option shown with its default value.

Full config example
ts
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:

text
${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 use
ℹ️Backward compat: if storage.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).

JSON driver
ts
storage: { driver: 'json', path: './consenti-data' } // path is a directory
⚠️The json 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.

DriverRequiresNotes
node:sqliteNode 22.5+Built-in, zero compilation. Recommended for Node 22.5+.
better-sqlite3 / sqliteNode 20+, native addonFastest option. Requires a prebuilt binary or build toolchain. sqlite is an alias.
node-sqlite3-wasmNode 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:

Install SQLite peer dep
bash
# 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
SQLite config examples
ts
// 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

DriverConnection fieldNotes
postgresqluriPostgreSQL 13+. Connection pool via pool.
mysqluriMySQL 8+ / MariaDB 10.6+. Connection pool via pool.
mongodburiMongoDB 6+. Optional database override.

Each server driver is an optional peer dependency — install the one you need:

Install server driver peer dep
bash
npm install pg         # PostgreSQL
npm install mysql2     # MySQL / MariaDB
npm install mongodb    # MongoDB
Server driver config examples
ts
// 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

KeyTypeDefaultDescription
driverstring'json' (when storage omitted)Required within the storage object. Selects the storage engine (see tables above).
pathstring'./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.
uristringServer drivers only. Full connection string including credentials.
databasestringname in uriMongoDB only. Database name override.
host / portstring / numberAlternative to uri for server drivers.
user / passwordstringCredentials when not embedded in uri.
databasestringDatabase 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 varDefault fallbackPurpose
CONSENTI_ADMIN_EMAIL[email protected]Bootstrap super-admin email when no auth.adminEmail is set.
CONSENTI_ADMIN_PASSWORDConsenti@123Bootstrap super-admin password when no auth.adminPassword is set.
CONSENTI_ADMIN_JWT_SECRETrandom per restartJWT signing secret. Takes precedence over auth.jwtSecret.
⚠️A random CONSENTI_ADMIN_JWT_SECRET means all sessions are invalidated on restart. Always set a stable secret in production.

Shared options (all modes)

KeyTypeDefaultDescription
mode'local' | 'jwt' | 'oidc' | 'saml' | 'custom''local'Authentication strategy for the admin dashboard.
jwtSecretstringrandom on startHMAC-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.

local auth
ts
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
}
KeyRequiredDefaultDescription
adminEmailyesenv.CONSENTI_ADMIN_EMAIL'[email protected]'Email address for the bootstrapped super-admin user.
adminPasswordyesenv.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.
ℹ️The bootstrap user is only created when the database has zero admin users. Adding more users via the dashboard after first boot does not require restarting the server.

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.

jwt auth
ts
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:

expected JWT payload
json
{
  "sub": "user-id",
  "email": "[email protected]",
  "roles": ["super_admin"],
  "tenantId": "default"
}
⚠️Only HMAC-SHA256 (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.

oidc auth
ts
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
    },
  },
}
KeyRequiredDefaultDescription
oidc.issueryesOIDC provider base URL. Must expose /.well-known/openid-configuration.
oidc.clientIdyesOAuth 2.0 client ID registered with the provider.
oidc.clientSecretyesOAuth 2.0 client secret.
oidc.redirectUriyesCallback URL registered with the provider. Must be: {basePath}/auth/oidc/callback.
oidc.claimsMapping.emailno"email"JWT claim that holds the user's email.
oidc.claimsMapping.rolesno"consenti_roles"JWT claim that holds an array of Consenti role names.
ℹ️The PKCE 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.

install peer dependency
bash
npm install samlify
saml auth
ts
auth: {
  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',
  },
}
KeyRequiredDescription
saml.issueryesIdP entity ID / issuer string.
saml.entryPointyesIdP SSO endpoint URL (HTTP-Redirect binding).
saml.certyesIdP X.509 signing certificate in PEM format (without the -----BEGIN CERTIFICATE----- headers).
saml.callbackUrlyesYour 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.

custom auth
ts
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(),
    }
  },
}
ℹ️In 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.

TOTP helpers (server-side)
ts
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)
ℹ️Each TOTP token is single-use: 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.

KeyTypeDefaultDescription
basePathstring'/consenti'All public API, admin API, and dashboard routes are mounted under this prefix.
basePath examples
ts
// 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/docs

dashboard

Controls whether Consenti serves the built-in Preact admin SPA. Accepts a boolean or a config object.

ValueEffect
trueServe the dashboard at {basePath}.
false / omittedDashboard files are not served; admin API routes remain active.
{ enabled: true, path: '/admin' }Serve at a custom sub-path instead of basePath.
dashboard examples
ts
// Simplest — serve at /consenti
dashboard: true

// Disable (headless / API-only mode)
dashboard: false

// Serve at a custom path
dashboard: { enabled: true, path: '/admin' }
ℹ️The dashboard SPA is a static bundle bundled into the package. No separate build step is needed — it is served directly from the @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.

KeyTypeDefaultDescription
enabledbooleantrueSet to false to disable rate limiting entirely (e.g. behind your own proxy that already rate-limits).
windowMsnumber60000Rolling window duration in milliseconds.
maxRequestsnumber60Maximum requests per IP per window. Requests over the limit receive 429 Too Many Requests.
rateLimit examples
ts
// 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 }
ℹ️Rate-limit counters live in process memory. For multi-instance deployments, disable the built-in limiter and implement a shared counter (Redis, etc.) at your load balancer or API gateway layer.

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 IDModelKey regulations
opt-inOpt-in (GDPR)GDPR, UK-GDPR, nFADP, KVKK, PDPA-TH, Middle East laws
opt-outOpt-out (US state laws)CCPA, VCDPA, CPA-CO, CTDPA, UCPA, 15 other US state laws
opt-out-strictOpt-out strict (CPRA)CPRA / California — GPC mandatory, sale/sharing categories
opt-in-dpdpaOpt-in (India)DPDPA — no Legitimate Interest, data fiduciary disclosure
opt-in-chinaOpt-in (China)PIPL, DSL, CSL — strict opt-in, no LI
opt-in-brazilOpt-in (Brazil)LGPD — opt-in with LI allowed under impact assessment
general-privacy-consentGeneral consentPIPEDA, POPIA, APPI, PDPA-SG/MY, 40+ others
notice-onlyNotice onlyJurisdictions with notice requirements but no consent mandate

compliance field reference

KeyTypeDefaultDescription
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.
complianceMapUrlstringRemote URL for the compliance map JSON. Only used when autoComplianceMap: 'auto'.
ℹ️GPC handling (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.
compliance examples
ts
// 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 }
  },
}
ℹ️The embedded compliance map covers 240+ countries with 8 compliance groups. To update jurisdiction mappings without upgrading the package, supply a custom 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.

KeyRequiredDescription
enabledyesSet to true to activate S3 sync.
regionyesAWS region, e.g. 'us-east-1'.
bucketNameyesS3 bucket name. Must allow PutObject on profiles/*.
accessKeyIdyesAWS access key ID.
secretAccessKeyyesAWS secret access key.
sessionTokennoAWS session token. Required only for temporary credentials (STS assume-role).
s3Api example
ts
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.

ArgumentTypeDescription
pathsstring[]Absolute file paths written or removed in this operation.
versionnumberCurrent profile version at the time of the operation.
isPurgebooleantrue — files were removed (deactivate / delete / update); false — files were written (create / activate).
handleCache example
ts
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.

KeyTypeDefaultDescription
enabledbooleanfalse (omit tcf to disable)Enable TCF mode. Starts the GVL refresh background job on boot.
cmpIdnumberYour IAB-registered CMP ID. Required when enabled: true.
cmpVersionnumberYour CMP software version number. Required when enabled: true. Included in TC strings.
tcf example
ts
tcf: {
  enabled: true,
  cmpId: 42,        // your IAB CMP ID
  cmpVersion: 1,
}
⚠️TCF mode requires an IAB-registered CMP ID. Do not use a placeholder CMP ID in production — the GVL and downstream DSPs validate it.

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.

KeyTypeDefaultDescription
enabledbooleanfalseEnable age gating.
minimumAgenumber13Minimum age required to record consent. Common values: 13 (COPPA), 16 (GDPR Article 8 default).
requireParentalConsentbooleanfalseWhen true, users below minimumAge must provide a parental consent token instead of being rejected outright.
ageGate examples
ts
// 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.

KeyTypeDefaultDescription
purgeAfterDaysnumber— (omit to disable)Delete consent records older than this many days. The purge runs once at startup and then every 24 hours.
dataRetention example
ts
// Keep consent records for 1 year, then purge automatically
dataRetention: {
  purgeAfterDays: 365,
}
ℹ️Audit log entries (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.

KeyTypeDefaultDescription
enabledbooleanfalseEnable multi-tenant mode. All requests without a valid tenant ID fall back to "default".
multiTenant example
ts
multiTenant: { enabled: true }

// Clients must then pass:
// X-Tenant-ID: acme-corp
// or: GET /consenti/api/v1/consent?tenantId=acme-corp

maxBodySize

Maximum allowed request body size in bytes. Requests exceeding this limit receive a 413 Payload Too Large response before any parsing occurs.

KeyTypeDefaultDescription
maxBodySizenumber1048576 (1 MB)Body size limit in bytes.
maxBodySize example
ts
maxBodySize: 512_000   // 512 KB

trustedProxies

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.

KeyTypeDefaultDescription
trustedProxiesstring[][]IP addresses or CIDR blocks of trusted reverse proxies.
trustedProxies examples
ts
// 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']
⚠️Only list IPs you actually control. A spoofed 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.

plugin skeleton
ts
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

HookWhen it firesCan 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
ℹ️Hooks that return a value (before* hooks) must return the complete modified object — not a partial. Hooks that do not return a value (after* hooks) receive a read-only snapshot; mutating it has no effect on the stored data.

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.

KeyTypeDefaultDescription
appNamestring'Consenti'Product name shown in the dashboard header, login page, and browser tab.
appLogoPathstringConsenti default logoPath 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.
hidePoweredBybooleanfalseWhen true, removes the "Powered by Consenti" badge from the dashboard footer.
branding examples
ts
// 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',
}
ℹ️Branding is applied once at startup — changing it requires a server restart. The logo and config are written to the built dashboard directory so they are available immediately when the first request hits the SPA.

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:crypto HMAC-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')