Audit Log API
Align records an audit trail of mutations and access-control denials. Organization admins can retrieve it through a filterable API.
The application only ever inserts into the audit table, and the database backs that up:
a trigger on audit_logs rejects any UPDATE, DELETE or TRUNCATE against an
existing row, for every role, including the one the application itself connects as. A
wrong entry is corrected by writing a new row, never by editing or removing one.
This does not stop the table's owner disabling or dropping the trigger by DDL
(ALTER TABLE ... DISABLE TRIGGER, DROP TRIGGER), or a superuser additionally
setting session_replication_role = replica for a session. Those two need different
privileges. Measured on PostgreSQL 16.15 with a NOSUPERUSER table owner:
session_replication_role is refused with permission denied to set parameter, while
the DDL succeeds and the write then goes through. So an owner who is not a superuser has
the DDL route and not the replication-role one, and no in-database mechanism can stop the
owner of a table. Each of those is a deliberate, auditable act rather than an
accidental or casual mutation, which is the property this control actually gives you.
Endpoint
GET /admin/audit-logs
- Auth: authenticated request (session cookie or
Authorization: Bearer <token>), org_admin role required (403 otherwise). - Scope: results are restricted to the caller's tenant.
Query parameters
| Parameter | Type | Description |
|---|---|---|
action | string | Filter by action. Values are namespace:verb with a colon, e.g. access:denied, decision:deleted, connector:enabled, data:exported. Exact match only. |
actor | string | Filter by the actor (user) who performed the action. |
resourceType | string | Filter by resource type. |
resourceId | uuid | Filter by a specific resource. |
from | ISO 8601 | Start of the time range. |
to | ISO 8601 | End of the time range. |
limit | int (1-1000) | Page size (default 100). |
offset | int | Pagination offset. |
Example
curl -s "$ALIGN_API/admin/audit-logs?action=access:denied&from=2026-06-01T00:00:00Z&limit=50" \
-H "Authorization: Bearer $ADMIN_TOKEN"
Each entry includes the actor, action, resource, a details object, and a timestamp.
Coverage and scope
What is recorded today: access-control denials (including the org_admin guard on
this endpoint and the five other admin endpoints that share it - data export, right
to erasure, legal hold, session revocation - when the caller is authenticated),
connector enable/disable, credential access, an OAuth connection completing and a
workspace being linked, MCP tool invocation, decisions being created, detected or
deleted, a decision's visibility changing, permission grants and revocations,
decision-link reconciliation, user role, permission and activation changes,
authentication events (login, logout and the authentication-failure cases below),
a GitHub webhook failing to resolve or post a comment against a linked ticket, and
the GDPR data-lifecycle events (export, deletion, legal hold, session revocation).
Authentication failures are recorded only when the request carries a known tenant: a
deactivated account's login attempt, an OIDC token with no email claim, and a revoked
token or session presented again. A request with no token, an invalid or expired
token, an unmatched API token, or a missing tenant header in demo or service-token
mode is rejected but not written to this table - audit_logs is tenant-scoped, and
writing a row would mean inventing a tenant for a caller we cannot identify. That
would also let an unauthenticated caller drive unbounded writes into the audit table.
Legal hold applies to decision snapshots, not to audit records - see
Data Retention. No retention job purges audit_logs, so the
trail is retained for the life of the tenant.