Skip to content

Audit log

Append-only record of state-changing admin actions — who did what, when, against what target.

The audit log captures every state-changing call to /api/v1/admin/* — settings updates, hook saves, admin invites, backup triggers, and so on — every collection write at /api/v1/collections, and every MCP tool call. One row per request, append-only, designed for compliance and forensic lookups.

A row is written for every POST / PUT / PATCH / DELETE to /api/v1/admin/* and to /api/v1/collections (create, update, delete a collection). GET is not audited — read traffic is high-volume and already covered by the request log.

Skip-list (intentionally not audited):

  • /api/v1/admin/auth/login, /api/v1/admin/auth/logout — auth attempts already log via the request log
  • /api/v1/admin/setup — fresh-install bootstrap
  • /api/v1/admin/migrations/diff — read-only despite being POST
  • Any path matching /api/v1/admin/preview-* or /api/v1/admin/.*/preview$

Each row is immutable. The application never updates audit rows, and deletes them only through the opt-in audit.retention_days setting (entries older than that many days are removed by the daily cleanup; 0, the default, keeps them forever). Operators can also DELETE directly from the vaultbase_audit_log table.

CREATE TABLE vaultbase_audit_log (
id TEXT PRIMARY KEY,
actor_id TEXT, -- admin id from the JWT
actor_email TEXT, -- email cached at the time of action
method TEXT NOT NULL,
path TEXT NOT NULL,
action TEXT NOT NULL, -- logical label, e.g. "collections.create"
target TEXT, -- best-effort id or name from the path
status INTEGER NOT NULL,
ip TEXT, -- client IP (see IP capture); null for MCP rows
summary TEXT, -- optional human / JSON
at INTEGER NOT NULL -- unix-seconds
);

Indexes on (actor_id), (at DESC), (action).

action is derived from the path + method. Examples:

MethodPathaction
POST/api/v1/collectionscollections.create
PATCH/api/v1/collections/<id>collections.update (target = the id)
PATCH/api/v1/admin/settingssettings.update
POST/api/v1/admin/hookshooks.create
POST/api/v1/admin/adminsadmins.create
POST/api/v1/admin/sql/runsql.create (target = run; the SQL text is not stored)

Uncategorised paths fall back to <METHOD> <path>.

MCP tool calls are entries with action mcp.tool, method MCP and the tool name as path; run_sql writes mcp.sql.read / mcp.sql.write. See MCP audit trail.

GET /api/v1/admin/audit-log
?page=1&perPage=50
&actorId=<id>
&actionPrefix=collections.
&from=<unix-seconds>&to=<unix-seconds>

Returns:

{
"data": {
"data": [ { "id": "...", "actor_email": "ops@acme", "action": "collections.delete", ... } ],
"page": 1,
"perPage": 50,
"totalItems": 1234,
"totalPages": 25
}
}

Sidebar → System → Audit log. Filter by actor id, action prefix, and date range; click a row to see the full entry (path, status, target, IP, timestamp, summary). The table paginates server-side; default page size 50.

The audit row’s ip field is the connecting peer’s address. X-Forwarded-For is used only when the peer is a trusted proxy (VAULTBASE_TRUSTED_PROXIES or security.trusted_proxies). Without that, a client can’t put a fake address in the column by sending the header. MCP tool calls have no request to read from, so their ip is null.

/etc/systemd/system/vaultbase.service
Environment=VAULTBASE_TRUSTED_PROXIES=10.0.0.0/8,127.0.0.1/32

Same env var the rate-limiter uses for its real-IP resolution.