← Documentation

Audit Logging

Every configuration change is recorded to a per-account AuditLog table and exposed through a read-only API.

The AuditLog model

Each entry belongs to one account and records who did what to which resource. The fields are:

FieldMeaning
actorType"vendor" | "customer_admin" | "system" | "api_key"
actorIdVendor user id, API key id, or the admin-portal session email — whichever identifies the caller
actionA free-form string like "connection.created" or "api_key.deleted"
resourceTypee.g. "connection", "api_key", "scim_config"
resourceIdThe affected record's id, when applicable
detailsArbitrary structured JSON context — call sites are written to never put secrets in it
ipAddressrequest.ip
userAgentRequest User-Agent header
severity"info" | "warn" | "error" | "critical", defaults to "info"

Two indexes back the two query patterns the API actually uses: (accountId, createdAt) for “recent activity in this account”, and (action, createdAt) for “every occurrence of this action over time”.

How entries get written

A single shared helper, recordAudit(), is the only code path allowed to write to AuditLog. It's fire-and-forget by design: the write happens in the background and the function never throws or awaits, so a slow or failed audit write can never break the request it's describing — failures are logged instead. Severity isn't computed automatically; each call site sets it by convention (deleting an API key is logged at warn, most configuration changes at info).

Querying audit logs

Every route checks that the caller belongs to the account being queried, the same requireOrgAccess check described in Access Control. Two credentials reach it. A vendor JWT is role-checked: all three routes are reads, so each asks only for the viewer minimum, and any of the three roles satisfies them. An API key is scope-checked instead: it must carry audit:read, and roles play no part — a key has no membership row, so the role comparison is never reached on that path. There is no customer-portal access to audit data.

GET/api/v1/audit

Lists entries for one accountId (required query parameter), optionally filtered by action, severity, actorType, and since, paginated with page/limit (capped at 200 per page).

GET/api/v1/audit/:id

Fetches a single entry by id.

GET/api/v1/audit/summary

Returns counts grouped by action and by severity over a window (since, defaulting to the last 7 days) — useful for a dashboard tile rather than a full log view.

curl -H "Authorization: Bearer $VENDOR_JWT" \
  "https://your-realmsso/api/v1/audit?accountId=acc_123&severity=warn&limit=50"

No retention or expiry policy yet

There is a separate OBSERVABILITY_EVENT_RETENTION_DAYS environment variable (default 90), but it governs ObservabilityEvent records — login attempts, SCIM sync results, connection health checks — not AuditLog. Nothing in the codebase currently deletes or archives old audit log rows, so they accumulate indefinitely unless an operator prunes the table themselves. Plan storage and any compliance-driven retention/deletion policy accordingly.