Audit log
Record of state-changing dashboard actions. Convention-append-only, no hard-delete enforced yet.
The audit log records actions performed through the dashboard (magic-link-authenticated humans). Ventus ops can use it for attestation; workspace owners use it for incident response.
API-key-driven actions live in a different surface. Session create/release/keepalive/profile-CRUD made via Bearer API tokens are emitted to the Redis event bus (and downstream observability), not to admin_audit_log. The audit table is for dashboard (human) actions. We plan to merge the two surfaces; for now they are separate.
What's recorded (admin_audit_log table)
Every dashboard action writes one row:
| Column | Example | Notes |
|---|---|---|
id | 9421 | BigInteger, autoincrement primary key. |
workspace_id | 01JC… (ULID, 26 chars) | Foreign key to workspaces. |
actor_user_id | <UUID> | NOT NULL. The Supabase user id (UUID-shaped). |
actor_email | alice@acme.com | Snapshot at action time; preserved if the user later changes email. |
actor_is_system_admin | true/false | True for Ventus ops actions across tenants. |
action | api_key.create | Dotted-namespace verb. See below. |
target_kind | api_key | String(32), nullable. |
target_id | 01KR… | String(128), nullable. |
detail | {"name":"smoke-test","prefix":"vs_01KR…"} | JSONB. Filtered through a column-level allowlist guard. |
ip | 76.133.250.94 | Client IP, nullable. |
user_agent | Mozilla/5.0 … | Truncated to 512 chars, nullable. |
at | 2026-05-18T01:16:41Z | UTC. Indexed (workspace_id, at). |
Retention + immutability — what's actually enforced
Today the admin_audit_log table has no Postgres trigger blocking UPDATE/DELETE and no retention job. Append-only and 7-year retention are application-level conventions, not database-enforced guarantees. The migration creates the table plus three indexes (workspace_id+at, action, at) — nothing more.
What that means in practice:
- The application code never issues
UPDATEorDELETEagainst
admin_audit_log. A DBA with direct Postgres access could.
- Recordings (session video) have a real retention policy
(recording_retention_days, default 30, max 3650 = 7 years per HIPAA §164.316(b)(2)) — that's where the "7 years" number comes from in our HIPAA story. It does not apply to the audit table yet.
Hardening on the roadmap: a Postgres trigger that raises on UPDATE/DELETE, a daily reaper job for retention enforcement, and a separate write-only DB role for the application.
Actions you'll see
Verbs emitted by the dashboard backend today:
api_key.create,api_key.revokemember.invite,member.accept,member.role_change,member.removeworkspace.create,workspace.update,workspace.deletesession.force_stop(admin DELETE, captured here too)
Session create/release/keepalive/expired and profile CRUD do not land in this table — they go through the Bearer-API path and are emitted to the event bus instead.
Read the log
From the dashboard: Audit in the top nav. Filters: actor, action namespace, target kind, date range. Default sort: newest first.
From the API: GET /v1/dashboard/ws/<slug>/audit?cursor=…&limit=100. Cursor-paginated; entries are immutable in practice (see above) so cursor positions are stable.
curl https://api.sessions.ventus.ai/api/v1/dashboard/ws/$WS_SLUG/audit \
-H "Authorization: Bearer $DASHBOARD_SESSION_JWT" \
-G --data-urlencode "since=2026-05-01T00:00:00Z" \
--data-urlencode "limit=100"What's not in the audit log
detail is JSONB but filtered through a column-level allowlist guard at write time (per the v0.4 audit C10 finding). The allowlist admits short, structured fields like name, prefix, role, region, slug. The dashboard code never emits raw URLs, response payloads, or request bodies into detail.
If you need to reconstruct what a specific session did inside the browser, you instrumented your own automation code; the audit table won't help. That's the privacy posture by design.
Export
There is no first-class export endpoint. To pull a window for a compliance request, page through the API:
curl https://api.sessions.ventus.ai/api/v1/dashboard/ws/$WS_SLUG/audit \
-H "Authorization: Bearer $DASHBOARD_SESSION_JWT" \
-G --data-urlencode "since=$START" --data-urlencode "until=$END" \
--data-urlencode "limit=10000" \
| jq -c '.items[]' > audit-$START-$END.ndjsonThe result is newline-delimited JSON; one object per row in the schema above.