Scenarios

Compliance audit

A tamper-evident audit pipeline that ships to your secure backend — typed actions via an audit catalog, signed chain optional.

The problem

You need to record every "this user did this thing" event in a way that satisfies a compliance auditor: typed actions, structured actor/target metadata, optionally tamper-evident, and shipped to a backend separate from your regular logs (so a leak of one doesn't compromise the other).

The full code

1. Define your audit catalog

src/audit-catalog.ts
import { defineAuditCatalog } from 'evlog'

export const billingAudit = defineAuditCatalog('billing', {
  INVOICE_REFUND: { target: 'invoice' },
  INVOICE_VOID: { target: 'invoice' },
  SUBSCRIPTION_CANCEL: { target: 'subscription' },
  PAYMENT_METHOD_REPLACE: { target: 'payment_method' },
} as const)

declare module 'evlog' {
  interface RegisteredAuditCatalogs {
    billing: typeof billingAudit
  }
}

2. A custom drain for the audit-only sink

Audit events go to a separate ingest that has stricter access controls than the regular logs.

src/audit-drain.ts
import { defineHttpDrain } from 'evlog/toolkit'

export function createAuditDrain(overrides?: { url?: string; token?: string }) {
  return defineHttpDrain<{ url: string; token: string }>({
    name: 'compliance-audit',
    resolve: () => ({
      url: overrides?.url ?? process.env.AUDIT_INGEST_URL!,
      token: overrides?.token ?? process.env.AUDIT_INGEST_TOKEN!,
    }),
    encode: (events, config) => ({
      url: `${config.url}/v1/audit-events`,
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${config.token}`,
      },
      // Filter: ship only events that carry an `audit` block
      body: JSON.stringify(events.filter(e => e.audit)),
    }),
  })
}

3. (Optional) Wrap with the signed chain for tamper-evidence

import { signed } from 'evlog'

const drain = signed({
  drain: createAuditDrain(),
  secret: process.env.AUDIT_CHAIN_SECRET!,
})

Each event then carries a audit.chain.{prevHash, currentHash} — a missing or modified event breaks the chain and can be detected at audit time.

4. Wire it up

server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline()
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', pipeline(
    createAxiomDrain(),         // regular logs go here
    createAuditDrain(),         // audit-only events go here
  ))
})

5. Record audit actions in handlers

server/api/billing/refund.post.ts
import { billingAudit } from '~/src/audit-catalog'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const body = await readBody(event)

  await processRefund(body.invoiceId, body.amount)

  log.audit(billingAudit.INVOICE_REFUND({
    actor: { id: getUser(event).id, type: 'user' },
    target: { id: body.invoiceId },
    metadata: { amount: body.amount, reason: body.reason },
  }))

  return { ok: true }
})

What it gives you

  • Typed audit actionsbillingAudit.INVOICE_REFUND(...) autocompletes parameters, the action key is fixed
  • Separate sink — audit events ship to a dedicated backend; regular logs flow normally to Axiom
  • Optional tamper evidence — the signed chain catches tampering after the fact
  • Standard wide events — auditors get the same context (user, request id, IP, …) every event has, plus the audit-specific block

Where to go next

  • Audit overview — the full audit primitives reference
  • Audit compliance — what specifically satisfies SOC2 / HIPAA-style requirements
  • Catalogs as packages — share the catalog as @my-org/evlog-billing-audit if multiple services emit billing actions
  • Custom drains — the building block used for the audit-only drain