Skip to content

Audit log & non-repudiation

Every sensitive operation appends an immutable row to audit_logs. Recording is fire-and-forget: an audit failure never breaks the request that triggered it (lib/audit.ts → recordAudit, mirroring the stats pattern).

Each row captures the actor, the resource, the request’s IP and user-agent, free-form metadata (old/new values on mutations), and an immutable created_at. The audited actions (SPECS §10.3):

auth.login · auth.logout · auth.password_reset
project.create · project.update · project.delete
integration.create · integration.update · integration.revoke
api_key.create · api_key.revoke
plan.create · plan.update · plan.delete · plan.assign
user.create · user.update · user.delete · user.plan_change
file.upload · file.update · file.delete · file.download
stats.read · audit.read

actor_type is one of admin | project | integration (a DB CHECK):

  • an admin session → admin, actor_id = the admin user id;
  • a project session → project, actor_id = the project id;
  • an integration key, or a user/space token (both integration-delegated), → integration, with the specific credential in actor_key_id.

user_id names the end-user the action concerns; project_id scopes the row to a tenant (null for admin-global actions).

Both endpoints are read-only and newest-first with cursor pagination (limit 1–100, default 50; cursor). Filters: action, user_id, actor_type, from / to (ISO timestamps).

  • GET /project/audit — a project reads its own logs. Owner-only (a project session; integration/user/space tokens are rejected). Scoped to the session’s project.
  • GET /admin/audit — the super admin reads all logs, plus an extra project_id filter. Admin-gated.
Terminal window
# a project's own recent file activity
curl "$BASE/project/audit?action=file.upload&limit=20" -H "Authorization: Bearer <project-jwt>"
# admin, one project, a date window
curl "$BASE/admin/audit?project_id=<uuid>&from=2026-08-01T00:00:00Z" --cookie "session=<admin>"

The read itself records an audit.read entry.

The retention window lives in system_settings under audit_retention_days (default 365). A daily interval in the API entrypoint purges rows older than the window.

Terminal window
GET /admin/settings/audit-retention # → { retention_days: 365 }
PUT /admin/settings/audit-retention # { "retention_days": 730 }