Consenti

Plugin System

Plugins extend the Consenti backend with custom lifecycle hooks. They run server-side, are instantiated once per createConsenti() call, and can access the full storage adapter.

Plugin contract

ts
import { ConsentiServerPlugin } from '@consenti/api'
import type { ConsentDbRecord, CreateConsentInput, PluginContext } from '@consenti/api'

export class MyPlugin extends ConsentiServerPlugin {
  name = 'my-plugin'

  async initialize(ctx: PluginContext): Promise<void> {
    // Called once after storage.connect() resolves.
    // ctx.storage gives you the full StorageAdapter.
    // ctx.config gives you the server config.
  }

  async destroy(): Promise<void> {
    // Called on graceful shutdown — close connections, clear timers.
  }

  // ── Optional hooks ──────────────────────────────────────────────────────────

  async beforeConsentSave(data: CreateConsentInput): Promise<CreateConsentInput> {
    return data  // mutate or validate before DB write
  }

  async afterConsentSave(record: ConsentDbRecord): Promise<void> {
    // fires after consent is persisted — perfect for webhooks, analytics
  }

  async afterConsentUpdate(record: ConsentDbRecord): Promise<void> {
    // fires after PUT /consent/:visitorId
  }
}

Registering plugins

ts
import { createConsenti } from '@consenti/api'
import { WebhookPlugin } from '@consenti-plugin-webhook'
import { SlackPlugin } from '@consenti-plugin-slack'

const app = createConsenti({
  storage: { driver: 'sqlite', path: './consenti.db' },
  auth: { mode: 'local', adminEmail: '[email protected]', adminPassword: 'secret' },
  plugins: [
    new WebhookPlugin('https://your-endpoint.example.com/hook'),
    new SlackPlugin({ webhookUrl: process.env.SLACK_WEBHOOK }),
  ],
})

Example: Webhook Notifier

Posts the consent record as JSON to a webhook URL after every save or update.

@consenti-plugin-webhook/src/index.ts
ts
import { ConsentiServerPlugin } from '@consenti/api'
import type { ConsentDbRecord } from '@consenti/api'

export class WebhookPlugin extends ConsentiServerPlugin {
  name = 'webhook'

  constructor(private webhookUrl: string, private secret?: string) { super() }

  async afterConsentSave(record: ConsentDbRecord): Promise<void> {
    await this.post(record)
  }

  async afterConsentUpdate(record: ConsentDbRecord): Promise<void> {
    await this.post(record)
  }

  private async post(record: ConsentDbRecord): Promise<void> {
    const headers: Record<string, string> = { 'Content-Type': 'application/json' }
    if (this.secret) {
      // HMAC-SHA256 signature so the receiver can verify authenticity.
      const { createHmac } = await import('node:crypto')
      headers['x-consenti-signature'] = createHmac('sha256', this.secret)
        .update(JSON.stringify(record))
        .digest('hex')
    }
    await fetch(this.webhookUrl, { method: 'POST', headers, body: JSON.stringify(record) })
  }
}

Package structure:

text
@consenti-plugin-webhook/
├── src/
│   └── index.ts
├── package.json
└── README.md
package.json
json
{
  "name": "@consenti-plugin-webhook",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "peerDependencies": {
    "@consenti/api": ">=0.1.0"
  }
}

Example: Slack Notifier

Sends a Slack message via Incoming Webhook after each consent submission.

@consenti-plugin-slack/src/index.ts
ts
import { ConsentiServerPlugin } from '@consenti/api'
import type { ConsentDbRecord, PluginContext } from '@consenti/api'

interface SlackOptions {
  webhookUrl: string
  channel?: string
}

export class SlackPlugin extends ConsentiServerPlugin {
  name = 'slack'
  private webhookUrl: string
  private channel?: string

  constructor(options: SlackOptions) {
    super()
    this.webhookUrl = options.webhookUrl
    this.channel = options.channel
  }

  async afterConsentSave(record: ConsentDbRecord): Promise<void> {
    const grantedCount = Object.values(record.consentJson).filter(s => s === 'granted').length
    const total = Object.keys(record.consentJson).length
    const text = `New consent recorded: ${grantedCount}/${total} categories granted${record.gpcDetected ? ' (GPC signal)' : ''}`

    const body: Record<string, unknown> = { text }
    if (this.channel) body['channel'] = this.channel

    await fetch(this.webhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    })
  }
}

Available hooks

HookWhen it fires
initialize(ctx)After storage.connect() resolves — once at startup
destroy()On graceful shutdown
beforeConsentSave(data)Before a new consent record is written — can mutate the payload
afterConsentSave(record)After a new consent record is persisted
beforeConsentUpdate(data)Before an existing consent record is updated
afterConsentUpdate(record)After an existing consent record is updated
beforeProfileFetch(id)Before a profile is fetched — can redirect to a different profile ID
afterProfileFetch(profile)After a profile is fetched — can mutate the profile
beforeUserCreate(data)Before an admin user is created
afterUserCreate(user)After an admin user is created

Error handling

If a plugin throws in any hook, Consenti logs the error with console.warn and continues — a broken plugin never blocks consent from being recorded.

Official plugins

Community plugins

Publish your own under any package name. The only requirement is extending ConsentiServerPluginand declaring @consenti/api as a peer dependency.

package.json
json
{
  "name": "@your-scope/consenti-plugin-custom",
  "peerDependencies": {
    "@consenti/api": ">=0.1.0"
  }
}