Skip to content

Audit Events Architecture

AuthHero uses the transactional outbox pattern to guarantee that audit events are captured atomically with entity mutations. This page covers the design decisions and trade-offs.

Why an Outbox?

Direct audit logging has two problems:

  1. Non-atomic writes. The entity mutation and the log write are separate operations. A crash between them loses the audit record silently.
  2. Non-transactional destinations. Some destinations (Analytics Engine, R2, webhooks) can never participate in a database transaction.

The outbox separates capture from delivery: the audit event is written to the database atomically with the entity mutation, then delivered asynchronously to each destination.

Transaction Boundary

When the outbox is enabled, each mutating management API request is wrapped in a single database transaction:

text
┌─────────────────────────────────────────────┐
│  DB Transaction (request-scoped)            │
│                                             │
│  1. Entity read (before state)              │
│  2. Entity write (UPDATE/INSERT/DELETE)     │
│  3. Outbox write (INSERT outbox_events)     │
│                                             │
│  COMMIT or ROLLBACK                         │
└─────────────────────────────────────────────┘

If the route handler throws (HTTP 404, 409, 500), the transaction rolls back — neither the entity mutation nor the outbox event is persisted.

Relay Processing

After the transaction commits, a background relay (via waitUntil) processes undelivered events:

  1. Fetch unprocessed events ordered by created_at
  2. For each event, transform and deliver to each destination
  3. Mark successful events as processed
  4. Retry failed events with exponential backoff (1s, 2s, 4s, ... up to 5 min)
  5. Clean up processed events past the retention period

The relay provides at-least-once delivery — destinations should be idempotent.

Sensitive Field Redaction

Before/after entity state is automatically redacted for sensitive fields: password, password_hash, client_secret, signing_keys, credentials, encryption_key, otp_secret. These appear as [REDACTED] in the audit event.

Database Considerations

SQLite (local development)

Native transaction support via WAL mode. No special considerations.

PlanetScale / MySQL (production)

When useTransactions is false in the database adapter options, the transaction() method is a passthrough — the outbox write is best-effort (non-atomic). This is a documented trade-off for PlanetScale deployments that don't support traditional transactions.

Cleanup and Retention

Processed events are deleted after a configurable retention period (default: 7 days). Cleanup runs inside every relay pass, so no dedicated cleanup job is required — but you do need a scheduled handler to run the relay itself as a safety net for events that failed inline delivery. See Outbox Relay (Cron) for the one-call runOutboxRelay helper and a Cloudflare Workers cron example.

Released under the MIT License.