Audit log
ProThe account's audit log. Rows hold ids and codes only, so names come from resolve; an id it can't resolve (a removed member, say) shows as the id, never a guess. Refusals are rows too, with their reason code. Filter by kind, or to refusals alone. Your app's own actions take their words from actions; one with no label shows its code, never a guessed verb.
Preview
Install
With the Candor CLI
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/audit-log
Brings in: locale, team, terms, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import { AuditLog } from "@/components/audit-log"
import type { AuditEvent } from "@/lib/team"
const events: AuditEvent[] = [
{ id: "e1", at: "2026-11-05T09:42:00Z", actorId: "u-priya", action: "approval.approved", target: { type: "approval_request", id: "ap-17" }, outcome: "ok", requestId: "req_9f2c" },
{ id: "e2", at: "2026-11-05T09:20:00Z", actorId: "u-dana", action: "role.set", target: { type: "environment_role", id: "u-jonas" }, outcome: "refused", reasonCode: "NOT_ADMIN_ON_ENVIRONMENT", requestId: "req_77a1" },
{ id: "e3", at: "2026-11-05T08:55:00Z", actorId: "agent-keel", action: "agent.tool_called", target: { type: "tool", id: "run_migration" }, outcome: "ok", requestId: "req_41d0" },
// The app's own actions: one labelled through `actions`, one not (its code shows as it is).
{ id: "e7", at: "2026-11-05T08:30:00Z", actorId: "u-dana", action: "connection.created", target: { type: "connection", id: "warehouse" }, outcome: "ok", requestId: "req_52aa" },
{ id: "e8", at: "2026-11-05T08:20:00Z", actorId: "system", action: "connection.synced", target: { type: "connection", id: "warehouse" }, outcome: "ok", requestId: "req_52ab" },
{ id: "e4", at: "2026-11-05T08:10:00Z", actorId: "u-priya", action: "member.invited", target: { type: "invite", id: "sam@acme.example" }, outcome: "ok", requestId: "req_31be" },
{ id: "e5", at: "2026-11-04T17:02:00Z", actorId: "system", action: "member.invite_expired", target: { type: "invite", id: "inv_4471" }, outcome: "ok", requestId: "req_0b5e" },
// A removed member: the id can't be resolved, so it shows as the id.
{ id: "e6", at: "2026-11-04T11:30:00Z", actorId: "u-x91", action: "audit.read", target: { type: "account", id: "acc_acme" }, outcome: "refused", reasonCode: "NOT_OWNER_OR_ADMIN", requestId: "req_a11c" },
]
const names: Record<string, string> = {
"u-priya": "Priya",
"u-dana": "Dana",
"u-jonas": "Jonas",
"agent-keel": "Keel migrations agent",
"ap-17": "Add region to customers",
run_migration: "run_migration",
warehouse: "Warehouse",
}
export default function Example() {
return <AuditLog className="w-full max-w-3xl" events={events} resolve={(id) => names[id]} actions={{ "connection.created": { label: "connected", group: "data" } }} />
}<AuditLog>
| Prop | Type | Default | Description |
|---|---|---|---|
| eventsrequired | AuditEvent[] | — | The rows, in any order; the newest shows first. |
| resolverequired | (id: string) => string | undefined | — | Turns a principal or target id into a display name, when known. |
| actions | Record<string, AuditActionInfo> | undefined | — | Labels and groups for your app's own actions, by code: `{ "connection.created": { label: "connected", group: "data" } }`. Also overrides a known action's label. |
| className | string | undefined | — | Extra classes for the outer element. |
Strings
The fixed words, in English by default. Change any of them under auditLog in <LocaleProvider strings={{ … }}>; the table is exported as auditLogStrings.
| Key | English |
|---|---|
| label | Audit log |
| kind | Kind |
| groupAll | Everything |
| groupMembers | People |
| groupRoles | Roles |
| groupApprovals | Approvals |
| groupAgent | Agent |
| groupData | Data |
| groupSessions | Sign-ins |
| groupOther | Other |
| refusalsOnly | Refusals only |
| empty | No events match. |
| notAMember | Not a current member |
| line | {actor} {action} {target} |
| refused | was refused: {action} |
| unlabelled | recorded {action} for |
| request | request {id} |
| signinSucceeded | signed in |
| signout | signed out |
| stepupCompleted | confirmed their identity |
| accountCreated | created the account |
| memberInvited | invited |
| memberInviteRevoked | revoked the invite for |
| memberInviteExpired | let expire the invite for |
| memberJoined | joined |
| memberRemoved | removed |
| roleSet | set a role for |
| roleRemoved | removed a role from |
| approvalRequested | requested approval |
| approvalApproved | approved |
| approvalSelfApproved | self-approved |
| approvalRejected | rejected |
| approvalWithdrawn | withdrew |
| approvalExpired | expired |
| approvalApplied | applied |
| auditRead | tried to read the audit log |
| approvalRead | tried to read approvals |
| agentToolCalled | called |
| agentApprovalRequested | asked for the go-ahead to run |
| agentApprovalGranted | allowed |
| agentApprovalDenied | turned down |
| agentApprovalExpired | let expire the go-ahead request to run |
| dataExported | exported data: |
| dataDeleted | deleted data: |
| dataRetentionChanged | changed how long data is kept: |
Types
- AuditActionInfo
- How AuditLog shows one of your app's own actions.
{ /** The verb, read between the actor and the target: "connected", "rotated the key for". */ label: string /** Which filter it falls under. Defaults to the group its prefix names ("member.", "data."), else Other. */ group?: AuditGroup | undefined }