Audit Log
Vetrix records every security-sensitive and privileged operation in an immutable audit log. Entries are append-only — no application endpoint can delete or modify them.
Querying the audit log
Instance-wide (admin only)
GET /api/v1/admin/audit-log?limit=100&offset=0
Authorization: Bearer <admin-jwt>
Per-repository
GET /api/v1/repos/{owner}/{repo}/audit-log?limit=50
Authorization: Bearer <jwt>
Response:
[
{
"id": 4712,
"actor_id": "550e8400-e29b-41d4-a716-446655440000",
"action": "acl.grant",
"resource": "repo:alice/myrepo",
"ip_address": "203.0.113.42",
"created_at": "2026-04-11T14:32:00Z"
}
]
Query parameters
| Parameter | Default | Description |
|---|---|---|
limit |
100 | Maximum entries to return (max 500) |
offset |
0 | Skip first N entries (pagination) |
Single-event detail (admin only)
GET /api/v1/admin/audit-log/{id}
Authorization: Bearer <admin-jwt>
Returns the full auditLogEntryView (same fields as the list endpoint)
for one event. {id} is the numeric BIGSERIAL primary key returned in the
list response.
| Status | Body |
|---|---|
| 200 | { id, actor_id, actor_username, action, resource, resource_type, resource_id, ip_address, user_agent, details, created_at } |
| 400 | {"error":"invalid audit id"} for non-numeric or non-positive id |
| 403 | {"error":"admin access required"} for non-admin callers |
| 404 | {"error":"audit entry not found"} |
Recorded actions
| Action | Trigger |
|---|---|
repo.create |
Repository created |
repo.delete |
Repository deleted |
repo.push |
Code pushed to a repository |
user.login |
Successful user login |
user.login_failed |
Failed login attempt |
user.logout |
User session ended |
token.create |
Personal access token created |
token.delete |
Personal access token revoked |
totp.enabled |
TOTP 2FA enabled for a user |
totp.disabled |
TOTP 2FA disabled |
acl.grant |
Collaborator role granted on a repository |
acl.revoke |
Collaborator role removed from a repository |
admin.user_created |
Admin provisioned a new user account |
admin.user_suspended |
Admin suspended a user account |
admin.settings_changed |
Instance settings updated |
pipeline.trigger |
CI/CD pipeline manually triggered |
pipeline.job.hard_failed |
A pipeline job reached a hard (non-tolerated) failure |
pipeline.job.soft_failed |
A pipeline job reached a soft (allow_failure) failure |
repo.protected_branch.updated |
A protected-branch rule was created or updated (incl. the require_pipeline gate toggle) |
repo.merge.pipeline_blocked |
A merge was refused by the required-pipeline gate |
repo.mr.pipeline_blocked |
A merge-request open was refused by the required-pipeline gate |
repo.push.pipeline_gate_blocked |
A push was rejected by the required-pipeline push gate |
scan.trigger |
Security scan manually triggered |
repo_transfer.preflight |
AccountTransfer preflight ran |
repo_transfer.executing |
AccountTransfer began moving the repository |
repo_transfer.completed |
AccountTransfer finished successfully |
repo_transfer.failed |
AccountTransfer aborted |
The four repo_transfer.* events form a chained record set. Each carries
the following details fields:
| Field | preflight | executing | completed | failed |
|---|---|---|---|---|
actor_user_id |
required | required | required | required |
source_repo_id |
required | required | required | required |
target_user_id |
required | required | required | required |
manifest_digest |
optional (set when pg_dump succeeds) | optional | required | optional |
error |
n/a | n/a | n/a | required |
The catalog accepts these event types without an audit_log schema
change — Action is a free-form string column. The hash-chain validator
covers them identically to legacy actions.
Database schema
The audit_log table has no DELETE grant in application code:
SELECT id, actor_id, action, resource, ip_address, user_agent, details, created_at
FROM audit_log
ORDER BY created_at DESC
LIMIT 100;
Exporting
For compliance exports, query directly via psql or a read replica:
psql "$DATABASE_URL" -c "\copy (
SELECT * FROM audit_log
WHERE created_at >= '2026-01-01'
ORDER BY created_at
) TO '/tmp/audit-2026.csv' WITH CSV HEADER"
Retention
Audit log entries are retained indefinitely by default. To implement a retention policy, run a scheduled database job — not an application delete:
-- Example: archive entries older than 2 years to a separate table.
INSERT INTO audit_log_archive SELECT * FROM audit_log WHERE created_at < NOW() - INTERVAL '2 years';
DELETE FROM audit_log WHERE created_at < NOW() - INTERVAL '2 years';