Immutable semantic security events, access boundaries, retention, and export workflows.
CentraKit audit logs are the product-facing security record for tenant and platform mutations. They are separate from operational telemetry, PostgreSQL/PGAudit diagnostics, and log drains, which remain debugging and infrastructure-observability tools.
The domain follows the standard provider-swappable service layers:
packages/platform/src/audit-log/ defines event schemas, the semantic event catalog, cursor encoding, errors, and AuditLogService.packages/supabase-platform/src/audit-log/ implements the service on the better-supabase audit block: record() writes semantic events through better_supabase.audit_event over a direct admin Postgres connection, passing the trusted actor, request ID and scope, and list() reads pages through list_audit_events and count_audit_events as the caller.packages/core wires request-scoped services and trusted actors; apps never import the provider directly.Coverage is hybrid. The better-supabase audit module, adopted over public.audit_logs and public.audit_log_restricted_details, captures every registered table mutation with its bs_audit row trigger as a durable baseline (each trigger carries its registration as a JSON argument, exemptions in sql.modules.audit.options.exempt), while application mutation adapters emit semantic events with business-level summaries, targets, outcomes, request IDs, correlation IDs, and idempotency keys. Auth and webhook boundaries use the same semantic model.
Audit recording must not add user-visible latency or turn a successful business mutation into a failure. Request paths schedule best-effort recording after the primary mutation; durable workflows record their own outcomes. Trigger capture remains the fallback for registered tables.
Audit rows are append-only: the module's bs_audit_append_only trigger rejects updates and deletes, and only better_supabase.purge_audit_log() deletes old entries. Row changes keep the changed column names on the entry and the old and new values of each changed column, redacted and cut at 1000 characters, in the restricted details. The module's own values are mapped onto CentraKit's (tenant scope to organization, support to platform_admin, oauth-client to integration).
The module's impersonation columns are mapped to impersonated_by, impersonation_reason and support_session_id. A row changed during a support session keeps the target as the actor, stored with the actor kind platform_admin, and records the admin, the reason and the session from the token's act claim, so audit_logs_support_session_idx lists everything one session did. The support-sessions module writes support.started and support.ended in platform scope, and the public.after_support_start and public.after_support_end hooks add one entry per organization of the target; all of them fill the three impersonation columns. Semantic events recorded through record() during a session carry the admin as the trusted actor and pass the session, the admin and the reason as supportSessionId, impersonatedBy and impersonationReason.
Safe metadata is limited to 4 KiB and rejects credential, cookie, password, secret, session, and token-like keys. Restricted request context is stored separately and excludes secrets and tokens. Organization views mask platform-admin actor identity. Revealing restricted fields requires a dedicated permission and goes through the module's reveal_audit_entry, which records an audit.revealed entry.
Overview lists use the block's offset pages with an exact filtered count (list({ offset, count: true })) so the table can share the same sort, filter, and pagination chrome as other hubs. Search matches the event type, summary, target, actor and organization labels and the target ID. Export workflows walk stable keyset cursors over (occurred_at, id) with the block's before cursor.
/{orgSlug}/audit-log and /{orgSlug}/audit-log/{eventId}/admin/audit-log and /admin/audit-log/{eventId}Organization reads are always scoped by the organization resolved from the route. Platform reads may span organizations. The navigation entries and page guards require auditLog.read or platform.auditLog.read.
Permissions are split by operation:
auditLog.read|export|revealplatform.auditLog.read|export|revealCSV exports are asynchronous Workflow jobs. Requests normalize a bounded date range, then the workflow reads cursor pages, applies row and byte limits, writes a private storage object, and exposes it for 24 hours. Restricted fields are available only for authorized platform exports. Export request, completion, failure, download, and reveal operations are audited.
The daily audit_log_retention job purges audit events with purgeAuditLog in bounded batches, using the public.audit_retention(organization) hook for each organization's interval (365 days for platform events), then a workflow deletes expired export objects before marking jobs expired. Default retention is 1 day for Starter, 7 days for Pro, 90 days for Business, and 365 days for Enterprise. Enterprise subscriptions may set a platform-managed organization override.
Last updated on