Audit Logging
AuthHero captures audit events for every management API mutation — user creation, role assignment, client updates, and more. Events include the actor, the affected entity (with before/after state), the request context, and the response.
Two Modes
Default Mode (outbox disabled)
Audit events are written as LogInsert records to the logs table via waitUntil. This is fire-and-forget — if the application crashes between the entity write and the log write, the audit record is lost.
Transactional Outbox Mode
When enabled, a rich AuditEvent is written to an outbox_events table alongside the entity mutation. A background relay then transforms and delivers events to destinations (logs table, and potentially Analytics Engine, R2, webhooks).
With transactions enabled (the default, useTransactions: true in the Kysely adapter), the outbox write and entity mutation share the same database transaction. This guarantees that if the entity write succeeds, the audit event is captured. If either fails, both are rolled back.
With useTransactions: false (passthrough mode), the outbox write is best-effort and not rollback-safe. The entity mutation and outbox insert are independent writes — if the entity write fails after the outbox write succeeds, the background relay may still deliver the audit event for a mutation that was never persisted.
Configuration
import { init } from "authhero";
const { app } = init({
dataAdapter,
outbox: {
enabled: true,
captureEntityState: true, // capture before/after entity state (default: true)
retentionDays: 7, // days to keep processed events (default: 7)
maxRetries: 5, // max delivery retries per event (default: 5)
},
});When outbox.enabled is false (or omitted), behavior is identical to the default mode.
The AuditEvent Type
Every audit event captures:
| Field | Description |
|---|---|
event_type | What happened — e.g. user.updated, role.created |
log_type | Auth0-compatible log type code (e.g. sapi) |
category | user_action, admin_action, system, or api |
actor | Who performed the action — type, ID, email, scopes, client_id |
target | What was affected — entity type, ID, before/after state, diff |
request | HTTP method, path, query, body, IP, user agent |
response | Status code and response body |
timestamp | ISO 8601 timestamp |
Before/After State
When captureEntityState is enabled, update operations capture the entity state before and after the mutation, plus a computed diff of changed fields. Sensitive fields (passwords, secrets) are automatically redacted.
How Events Flow
Request Handler
│
├─ Entity write (e.g., UPDATE user) ┐ Same DB transaction when
└─ Outbox write (INSERT outbox_events) ┘ useTransactions: true (default)
│
▼ waitUntil (after response)
Outbox Relay
│
├─ LogsDestination: AuditEvent → LogInsert → logs table
├─ (future) R2Destination: full event as NDJSON
└─ (future) WebhookDestination: filtered payloadQuerying Logs
The management API GET /api/v2/logs endpoints work identically in both modes. When the outbox is enabled, the relay populates the same logs table via the LogsDestination transformer.
Related
- Architecture: Audit Events — deep dive into the outbox pattern
- Outbox Adapter — adapter interface reference