These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

Decision: Audit headers on mutations

Status: accepted

Context

The backend needs to record a meaningful audit log for all write operations — who did what, to which resource, via which user-facing action. The server has raw HTTP details (method, route, payload, status code) but lacks user intent: a PUT /finders/42 could be a name change, a design update, a location sync, or a publish — all the same route, completely different audit meaning.

Two approaches were considered:

  1. Server-side inference — parse the request body and route to guess the action. Fragile; the same endpoint can mean different things in different UI flows.
  2. Frontend-named headers — the calling code annotates each mutation with the action it represents. The server records what it’s told.

The frontend knows user intent because it drives the UI flow. The Apply AI Batch action, for example, crosses multiple mutations; a stable correlation ID across all of them is only possible if the frontend generates it.

Decision

The frontend sends four optional HTTP headers on all POST, PUT, PATCH, and DELETE requests (isMutatingMethod check in core.ts:87):

HeaderWho sets itValue
X-Audit-Request-Idauto (crypto.randomUUID())UUID per mutation call
X-Audit-Actioncaller passes auditActionHuman label, e.g. "publish finder"
X-Audit-Resource-Typecaller passes auditResourceTypeResource noun, e.g. "finder"
X-Audit-Resource-Idcaller passes auditResourceIdResource primary key

X-Audit-Request-Id is always emitted for mutations even if the caller omits the others. The remaining three are optional — if absent, AuditLogSanitizer infers workspace and resource from the route.

The backend RecordAuditLog middleware (audit alias) reads these headers and writes an audit_logs row on every 2xx mutation response.

Trust boundary: The backend does not validate that the action label is accurate or matches the route. The headers are informational. Authorization is enforced separately by auth:api, role:admin, and subscription_tier:* middleware.

Consequences

Benefits:

  • Audit log entries are human-readable without joining against a route table.
  • Multi-step flows (AI import apply, bulk sync) get a consistent action label across all mutations.
  • New endpoints get an audit entry automatically; callers add labels at their discretion.

Costs:

  • Every mutation call site is responsible for passing meaningful label values. When callers omit them, the log entry falls back to server-side inference which may be less precise.
  • A malicious client could send a misleading X-Audit-Action. Acceptable because these headers supplement, not replace, server-side authorization.