Audit log

Every action that changes user-visible behavior is logged. Available to Admins on all tiers; CSV export and webhook streaming on Pro+. Two-store model — Postgres authoritative, ClickHouse for retention queries.

The audit log records every mutation across Sankofa: flag toggles, config publishes, deploy promotions, member changes, key rotations, project deletions, integration configuration changes, and Vision / Plan / Pulse content edits. It's the primary surface for "who changed what, when" — used for compliance, incident review, and on-call runbooks.

The implementation is at /server/engine/ee/audit/.

What's logged

Every product writes audit rows on every mutation. The schema is consistent across producers:

FieldDescription
actor_idUser UUID. For automation, pseudo-actors like system:halt-webhook or scim:sync are used.
actor_emailResolved email for the actor (when actor_id is a real user).
actionDomain.action verb — switch.flag.halted, plan.ticket.transitioned, deploy.release.promoted_to_100, etc.
resource_typeThe entity type (flag, config_item, release, member, api_key, project, webhook).
resource_idUUID of the affected resource.
beforeJSON snapshot of the entity before the mutation. May be null for creates.
afterJSON snapshot after the mutation. May be null for deletes.
metadataFree-form JSON — request ID, IP, user-agent, evidence URL on alert-driven actions.
created_atUTC timestamp.

Two-store model

StoreRoleRetention
Postgres audit_log_entriesAuthoritative store. Strict ordering, transactional consistency with the producer's mutation.All tiers — entries persist for the org's tier retention window.
ClickHouse audit_eventsQuery-optimized mirror for analytics + long-range queries. Async dual-write; failures logged but non-fatal.TTL 2 years, regardless of tier (overage is fine — ClickHouse is the cheap mirror).

The dashboard reads from Postgres for the latest 30 days, then falls back to ClickHouse for older queries.

Retention by tier

TierAudit retention
Hobby90 days
Pro1 year
Growth2 years
Enterprise7 years (SOX-compliant)

These windows apply to the Postgres store. The ClickHouse mirror keeps 2 years across all tiers, but only Enterprise tier surfaces the longer history in the dashboard.

Who can read it

  • Viewer / Editor: read-only access to the audit log for their project's mutations. The cross-project aggregate view is hidden.
  • Project Admin: read + filter for the project, plus CSV export (Pro+).
  • Organization Admin / Owner: read + filter across every project, plus CSV export and webhook stream configuration.

The dashboard surface is /dashboard/account/audit-log (org-wide) and per-project at /dashboard/<project>/audit.

CSV export

Available on Pro tier and above. Two flavors:

  1. On-demand

    Apply filters (date range, actor, action, resource), click Export CSV. The download streams from Postgres for the last 30 days, ClickHouse for older. File size cap: 100 MB per export — narrow filters or split by month.

  2. Scheduled

    Pro+: configure a scheduled export at /dashboard/account/audit-log → Scheduled exports → New. Daily, weekly, or monthly. Delivery via email link or pre-signed S3 / GCS URL (Growth+).

Webhook stream

Available on Pro+. Configure at /dashboard/account/audit-log → Webhook stream. Every audit row is delivered as an outbound webhook event — same delivery semantics as the outbound webhook framework:

  • HMAC-SHA256 signing
  • 10-second per-attempt timeout
  • Exponential-backoff retries up to 24 hours
  • Dead-letter queue with manual replay

The stream payload looks like:

JSON
{
"event": "audit.entry.created",
"organization_id": "org_xyz",
"emitted_at": "2026-05-09T14:32:01.482Z",
"data": {
  "id": "ale_abc123",
  "actor_id": "user_456",
  "actor_email": "alice@example.com",
  "action": "switch.flag.halted",
  "resource_type": "flag",
  "resource_id": "fl_xyz",
  "before": { "halted_at": null, "current_version": 7 },
  "after": { "halted_at": "2026-05-09T14:32:01.482Z", "current_version": 8 },
  "metadata": {
    "request_id": "req_abc",
    "ip": "203.0.113.42",
    "evidence_url": "https://app.sankofa.dev/dashboard/catch/issues/iss_abc"
  },
  "created_at": "2026-05-09T14:32:01.482Z"
}
}

SIEM integration

For organizations that route security telemetry into a SIEM (Splunk, Datadog Cloud SIEM, Sumo Logic, Elastic Security, etc.):

  • Webhook stream → your SIEM's HTTP receiver. Most SIEMs accept arbitrary JSON over HMAC-signed webhooks.
  • Scheduled CSV export → S3 + your SIEM's S3 ingest path.
  • API pollingGET /api/v1/audit?since=<timestamp> polled hourly. Pull-based, useful for SIEMs without webhook receivers.

The webhook stream is the lowest-latency path; CSV export is the cheapest at high volume.

API surface

EndpointAuthPurpose
GET /api/v1/auditJWT (Admin+)List audit entries with filters: project_id, actor_id, action, resource_type, since, until, limit.
GET /api/v1/audit/:idJWT (Admin+)Detail view of one audit entry.
POST /api/v1/audit/exportsJWT (Admin+)Create an on-demand CSV export. Returns a job ID; poll GET /api/v1/audit/exports/:id for status.
GET /api/v1/audit/exports/:idJWT (Admin+)Export job status + signed download URL once ready.

Common queries

QuestionFilter
"Who halted new_checkout flag?"action = switch.flag.halted AND resource_id = fl_xyz
"What changed during last week's incident?"since = 2026-05-04T00:00:00Z AND until = 2026-05-08T23:59:59Z
"Did Alice access this project's audit log?"actor_email = alice@example.com AND action = audit.export.requested
"All deploys promoted to 100% in May"action = deploy.release.promoted_to_100 AND since = 2026-05-01T00:00:00Z
"All key rotations"action = api_key.rotated

Action catalog (selected)

The audit log uses the same action namespace as the outbound webhooks. Common actions:

  • auth.signin, auth.signout, auth.password.changed
  • member.invited, member.role.updated, member.removed
  • team.created, team.member.added, team.project.assigned
  • project.created, project.deleted, project.region.changed
  • api_key.created, api_key.rotated, api_key.revoked
  • switch.flag.created, switch.flag.toggled, switch.flag.halted, switch.flag.unhalted
  • config.item.created, config.item.published, config.item.rolled_back
  • deploy.release.uploaded, deploy.release.promoted, deploy.release.disabled
  • pulse.survey.created, pulse.survey.published, pulse.response.submitted (configurable — opt-in)
  • plan.ticket.created, plan.ticket.transitioned
  • vision.board.created, vision.initiative.linked
  • webhook.subscription.created, webhook.delivery.failed
  • audit.export.requested, audit.export.delivered

What's next

Edit this page on GitHub