Audit Logs
The audit_logs table provides a tamper-evident record of mutations in your application. Each entry captures who did what, to which resource, and when.
Schema
| Column | Type | Description |
|---|
id | uuid | Primary key |
workspace_id | uuid (nullable) | Scopes the event to a workspace |
actor_id | uuid | The authenticated user who performed the action |
action | text | Dot-namespaced action string (e.g., member.invited) |
resource_type | text | What kind of resource was affected (e.g., invitation) |
resource_id | uuid (nullable) | ID of the affected resource |
metadata | jsonb (nullable) | Extra context (role, email, old values, etc.) |
ip_address | inet (nullable) | Request IP (populate manually if needed) |
created_at | timestamptz | When the event occurred |
Recording an Event
Import auditLog from @/lib/audit/log and call it after any mutation in a server action:
import { auditLog } from "@/lib/audit/log";
// Inside a server action
await supabase.from("invitations").insert({ ... });
await auditLog({
workspaceId: workspace.id,
action: "member.invited",
resourceType: "invitation",
resourceId: invitation.id,
metadata: { email: inviteeEmail, role },
});
The actor is captured automatically from the current auth session. The function never throws — if audit logging fails, it logs to console.error without breaking the calling action.
Action Naming Convention
Use dot-namespaced strings that read as noun.verb:
workspace.created workspace.deleted
member.invited member.removed member.role_changed
user.password_changed user.deleted
subscription.created subscription.canceled
Querying Audit Logs
Use AuditLogFilters to query with pagination:
import type { AuditLogFilters } from "@/lib/audit/types";
const filters: AuditLogFilters = {
workspaceId: "ws-123",
action: "member.invited",
page: 1,
perPage: 50,
};
RLS Policy
Audit logs are readable by workspace members (scoped to their workspace) and by super admins (all logs). Inserts are only allowed from authenticated sessions (actors can only log their own actions).
Join audit_logs with profiles to display actor names and emails. The AuditLogEntry type includes optional actor_email and actor_name join fields.
Adding Audit Logs to New Features
When adding a new server action that mutates data, add an auditLog() call as the last step:
- Perform the mutation.
- Check for errors and return early if needed.
- Call
auditLog(...) — fire and forget, no await needed if you don’t care about the result.