Consenti

Backend — Events

createConsenti() returns an eventBus — a standard Node.js EventEmitter — that emits typed events at every data lifecycle step. Subscribe to hook into consent saves, profile changes, and the server ready signal without polling the database or modifying routes.

Basic usage
ts
import { createConsenti } from '@consenti/api'

const { eventBus, ready } = createConsenti({ /* ... */ })

eventBus.on('consent.created', (record) => {
  console.log('New consent from visitor', record.visitorId)
})

// Wait for storage + bootstrap to complete before accepting requests
await ready

Event reference

EventEmitted whenPayload
readyStorage connected and admin user bootstrappednone
consent.createdA new consent record is saved for the first timeConsentDbRecord
consent.updatedAn existing consent record is updated{ previous: ConsentDbRecord, current: ConsentDbRecord }
consent.erasedA visitor's data is erased (GDPR right to erasure){ visitorId: string }
visitor.createdA new visitor record is createdVisitor
profile.createdA consent profile is createdProfile
profile.updatedA consent profile is updated (version bumped){ previous: Profile, current: Profile }
profile.deletedA consent profile is deleted{ id: string, previous: Profile }
cache:warmLocale JSON files are written to disk (profile create or activate){ paths: string[], version: number }
cache:purgeLocale JSON files are removed (profile update, deactivate, or delete){ paths: string[], version: number }

ready

Emitted once after the storage adapter connects, migrations run, and the bootstrap admin user is created (if needed). This maps to the ready Promise returned by createConsenti() — they resolve at the same time.

Use the Promise form when you need to await startup before accepting requests. Use the event form when you want a fire-and-forget side effect.

ts
// Promise form (preferred for startup sequencing)
const consenti = createConsenti({ /* ... */ })
await consenti.ready
server.listen(3000)

// Event form (fire-and-forget side effects)
consenti.eventBus.on('ready', () => {
  console.log('[consenti] backend ready')
})

consent.created

Fires after a new ConsentDbRecord is persisted — after plugins' afterConsentSave hooks run. Useful for webhooks, audit side-cars, or real-time analytics.

ts
import type { ConsentDbRecord } from '@consenti/api'

eventBus.on('consent.created', async (record: ConsentDbRecord) => {
  await fetch('https://webhook.example.com/consent-created', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      visitorId: record.visitorId,
      profileId: record.profileId,
      consent: record.consentJson,
      gpc: record.gpcDetected,
      timestamp: record.createdAt,
    }),
  })
})

consent.updated

Fires when a visitor submits new consent choices that differ from their existing record. The payload includes both the previous and current state so you can compute a diff.

ts
import type { ConsentDbRecord } from '@consenti/api'

eventBus.on('consent.updated', ({ previous, current }: {
  previous: ConsentDbRecord
  current: ConsentDbRecord
}) => {
  const prev = previous.consentJson
  const next = current.consentJson

  for (const cookieId of Object.keys(next)) {
    if (prev[cookieId] !== next[cookieId]) {
      console.log(`${cookieId}: ${prev[cookieId]} → ${next[cookieId]}`)
    }
  }
})

consent.erased

Fires after a visitor exercises their GDPR right to erasure. The visitor's consent record and visitor row are deleted from storage before this event fires.

ts
eventBus.on('consent.erased', ({ visitorId }: { visitorId: string }) => {
  // Mirror the deletion to a data warehouse or external DMP
  myDmpClient.deleteVisitor(visitorId)
})

visitor.created

Fires when Consenti creates a new visitor row — typically on first consent submission from that visitor. IPs are never available here; the record stores only the SHA-256 hash (ipHash).

ts
import type { Visitor } from '@consenti/api'

eventBus.on('visitor.created', (visitor: Visitor) => {
  console.log('New visitor in region:', visitor.region ?? 'unknown')
})

profile.created / profile.updated / profile.deleted

Profile lifecycle events. profile.updated fires whenever a profile version is bumped — useful for cache invalidation in edge deployments. The previous profile is included so you can compare changes.

ts
import type { Profile } from '@consenti/api'

eventBus.on('profile.created', (profile: Profile) => {
  console.log('New profile:', profile.id, 'v' + profile.version)
})

eventBus.on('profile.updated', ({ previous, current }: {
  previous: Profile
  current: Profile
}) => {
  console.log('Profile', current.id, 'bumped from v' +
    previous.version + ' to v' + current.version)
})

eventBus.on('profile.deleted', ({ id, previous }: { id: string; previous: Profile }) => {
  console.log('Profile deleted:', id, '(was v' + previous.version + ')')
})

cache:warm / cache:purge

Fired after Consenti writes or removes locale JSON files on disk. Use these to integrate with a CDN, nginx cache, or any edge layer that needs to be invalidated or pre-warmed when a profile changes.

cache:warm fires when new files are written (profile created or activated). cache:purge fires when files are removed (profile updated, deactivated, or deleted). Both mirror the handleCache config callback — use whichever suits your integration.

ts
eventBus.on('cache:warm', ({ paths, version }: { paths: string[]; version: number }) => {
  // paths — array of locale file paths written to disk, e.g.
  // ['./consenti-data/profiles/default/opt-in/en.json', '...fr-FR.json']
  for (const p of paths) {
    cdnClient.warmPath(p)
  }
})

eventBus.on('cache:purge', ({ paths, version }: { paths: string[]; version: number }) => {
  for (const p of paths) {
    cdnClient.purge(p)
  }
})
ℹ️The handleCache config option is equivalent to listening on both cache:warm and cache:purge:isPurge: falsecache:warm, isPurge: truecache:purge. Use whichever style fits your integration.
ℹ️All event handlers that perform async work should handle their own errors. Unhandled rejections inside event listeners are not caught by Consenti — use a try/catch or attach a .catch() to avoid crashing the process.