zazzy
Features

Audit log

Append-only record of significant actions with an admin viewer.

The audit log is an append-only record of significant actions — who did what, to what, when. Other features record events through a single write seam; there is no update or delete path. An admin-only viewer under settings reads the log back with filtering and gapless cursor pagination.

Optional feature

Audit log is optional (core: false, requires: ["orgs"]). Rows are org-scoped; the recording seam is a soft integration other features call behind @feature:audit-log markers.

What it does

  • Single write seamaudit(ctx, action, target, metadata?) (server/audit.ts) is the one write point: a single db.auditLog.create. It is fire-and-forget (D-017) — a failed write is logged and swallowed, never thrown into the caller.
  • Context captureAuditContext carries organizationId, actorId?, actorType (user | system | api-key), an @feature:api-keys-gated apiKeyId?, and ip?. userAuditContext({ … }) builds a user-actor context and derives the IP from X-Forwarded-For / X-Real-IP.
  • Read servicelistAuditLogEntries(db, params) is the single read source, newest-first ([createdAt desc, id desc], a total order), fetching limit + 1 to compute nextCursor without a count. Filters: action, actorId, from/to, cursor. DEFAULT_AUDIT_PAGE_SIZE = 50, MAX = 100.
  • Admin-gated routerauditLogRouter exposes one list query on orgProcedure.role("admin"), scoped to the caller's active org (no cross-org leakage).
  • ViewerAuditLogViewer at /settings/audit-log: a filter bar (action / actor / from / to, applied on submit), a newest-first table, per-row expandable metadata + IP, and a "Load more" button over useInfiniteQuery.

Models it adds

ModelPurpose
AuditLogOne append-only entry: actorId/actorType, action (dot-namespaced, e.g. member.role_changed), targetType/targetId, metadata (Json), and ip. actorId is a plain nullable column, not a User FK, so deleting the actor never cascades away history.

Adds the marker-wrapped Organization.auditLogs relation. org.delete is deliberately un-audited (D-060): the AuditLog rows cascade with the org, so an org.deleted row would be purged by the very delete it records.

Env it needs

None. metadata uses native Prisma Json, so no new dependency, script, or compose service. The append-only design has no update or delete surface.

Composition

  • requires: ["orgs"] — every row is org-scoped (AuditLog.organizationId cascades from Organization) and every viewer query runs on orgProcedure.role("admin").
  • Recording callers (soft, via @feature:audit-log markers — not features.json requires): core org/member/invite mutations call audit() directly; api-keys, flags, and files receive it as an injected audit? seam; billing's Stripe webhook records system-actor events; and admin both writes override/impersonation events and reads the tail through listAuditLogEntries. The central adapter is recordAudit in src/server/router.ts.
  • Dependents — none; no feature requires audit-log, so every caller stands alone when it is pruned.

What pruning removes

Leaving audit-log out drops features/audit-log, the /settings/audit-log route, the retrofit integration test, the AuditLog model, and the Organization.auditLogs relation — plus the recordAudit seam and the marked audit(...) blocks across the callers. The core mutations still run (their audit calls are additive and fire-and-forget); the injected-seam features fall back to createXRouter({}) with the optional audit?.(...) becoming a no-op; and admin loses its audit tail while otherwise standing.

On this page