Audit log

Pro

The 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.

Markdown

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

TSX · The example above
"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>

PropTypeDefaultDescription
eventsrequiredAuditEvent[]—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.
actionsRecord<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.
classNamestring | 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.

KeyEnglish
labelAudit log
kindKind
groupAllEverything
groupMembersPeople
groupRolesRoles
groupApprovalsApprovals
groupAgentAgent
groupDataData
groupSessionsSign-ins
groupOtherOther
refusalsOnlyRefusals only
emptyNo events match.
notAMemberNot a current member
line{actor} {action} {target}
refusedwas refused: {action}
unlabelledrecorded {action} for
requestrequest {id}
signinSucceededsigned in
signoutsigned out
stepupCompletedconfirmed their identity
accountCreatedcreated the account
memberInvitedinvited
memberInviteRevokedrevoked the invite for
memberInviteExpiredlet expire the invite for
memberJoinedjoined
memberRemovedremoved
roleSetset a role for
roleRemovedremoved a role from
approvalRequestedrequested approval
approvalApprovedapproved
approvalSelfApprovedself-approved
approvalRejectedrejected
approvalWithdrawnwithdrew
approvalExpiredexpired
approvalAppliedapplied
auditReadtried to read the audit log
approvalReadtried to read approvals
agentToolCalledcalled
agentApprovalRequestedasked for the go-ahead to run
agentApprovalGrantedallowed
agentApprovalDeniedturned down
agentApprovalExpiredlet expire the go-ahead request to run
dataExportedexported data:
dataDeleteddeleted data:
dataRetentionChangedchanged 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 }