Audit log
ProWho did what, to which record, and when. Events group by day and expand to their before and after values and request context; filter by actor, record type and time, and search across everything.
npx prodular-ui add audit-logPart of Prodular UI Pro. Preview and read the docs freely, install with a license. See pricing
Workspace audit log
People, an API key, an agent and the system. Open an event for its changes; long text shows as a word diff. Times are in UTC.
Today · UTC
Yesterday · UTC
"use client"
import { DownloadIcon } from "lucide-react"
import { AuditLog, type AuditActor, type AuditEvent } from "@/components/ui/audit-log"import { Button } from "@/components/ui/button"
const now = new Date(Date.UTC(2026, 9, 22, 16, 30))const at = (day: number, h: number, m: number) => new Date(Date.UTC(2026, 9, day, h, m))
const maya: AuditActor = { type: "user", name: "Maya Okafor", email: "maya@halcyonlabs.com", initials: "MO" }const priya: AuditActor = { type: "user", name: "Priya Raman", email: "priya@halcyonlabs.com", initials: "PR" }const theo: AuditActor = { type: "user", name: "Theo Lindqvist", email: "theo@halcyonlabs.com", initials: "TL" }const erpKey: AuditActor = { type: "api", name: "NetSuite sync key" }const analyst: AuditActor = { type: "agent", name: "Collections agent" }const system: AuditActor = { type: "system", name: "Relay" }
const office = { ip: "84.17.42.110", location: "Lisbon, Portugal", userAgent: "Chrome 141 on macOS" }
const events: AuditEvent[] = [ { id: "evt_9Q2c41", action: "member.role_changed", actor: priya, summary: ( <> <b className="font-medium text-foreground">Priya Raman</b> changed the role of <b className="font-medium text-foreground">Hana Sato</b> </> ), resource: { type: "member", id: "usr_hana", name: "Hana Sato" }, at: at(22, 15, 48), changes: [{ field: "Role", before: "Approver", after: "Accountant" }], metadata: { reason: "Covering month-end close" }, ...office, }, { id: "evt_9Q2b87", action: "invoice.updated", actor: theo, resource: { type: "invoice", id: "inv_2041", name: "INV-2041" }, at: at(22, 14, 12), changes: [ { field: "Due date", before: "Oct 31, 2026", after: "Nov 14, 2026" }, { field: "Amount", before: "$5,900.00", after: "$5,400.00" }, { field: "Note to customer", before: "Payment is due within 30 days of the invoice date.", after: "Payment is due by November 14. We applied the $500 credit agreed on the October call.", }, ], ...office, }, { id: "evt_9Q2a10", action: "reminder.sent", actor: analyst, summary: ( <> <b className="font-medium text-foreground">Collections agent</b> sent a payment reminder for <b className="font-medium text-foreground">INV-2057</b> </> ), resource: { type: "invoice", id: "inv_2057", name: "INV-2057" }, at: at(22, 11, 3), metadata: { approval: "apr_771", approved_by: "maya@halcyonlabs.com", channel: "email" }, }, { id: "evt_9Q29f2", action: "api_key.created", actor: maya, resource: { type: "api_key", id: "key_live_8f2a", name: "NetSuite sync key" }, at: at(22, 9, 41), changes: [{ field: "Scopes", before: null, after: "invoices:read, payments:write" }], severity: "warning", ...office, }, { id: "evt_9Q27c0", action: "payment.imported", actor: erpKey, resource: { type: "payment", id: "pay_5521", name: "ACH 5521" }, at: at(21, 22, 15), ip: "34.120.18.6", userAgent: "relay-node 4.2.0", metadata: { request_id: "req_7Fh3kQ", records: 42 }, }, { id: "evt_9Q26aa", action: "sso.enforced", actor: maya, summary: ( <> <b className="font-medium text-foreground">Maya Okafor</b> made single sign-on required for <b className="font-medium text-foreground">Halcyon Labs</b> </> ), resource: { type: "workspace", id: "ws_halcyon", name: "Halcyon Labs" }, at: at(21, 16, 2), changes: [{ field: "Password sign in", before: true, after: false }], severity: "critical", ...office, }, { id: "evt_9Q2510", action: "export.downloaded", actor: theo, resource: { type: "report", id: "rep_q3", name: "Q3 receivables" }, at: at(21, 10, 37), metadata: { format: "CSV", rows: 1288 }, ...office, }, { id: "evt_9Q23e1", action: "session.signed_in", actor: priya, resource: { type: "workspace", id: "ws_halcyon", name: "Halcyon Labs" }, at: at(20, 8, 55), ip: "91.205.12.4", location: "Warsaw, Poland", userAgent: "Safari 26 on iOS", }, { id: "evt_9Q2201", action: "plan.renewed", actor: system, resource: { type: "subscription", id: "sub_scale", name: "Scale plan" }, at: at(19, 0, 0), changes: [{ field: "Seats", before: 40, after: 42 }], },]
export default function AuditLogWorkspace() { return ( <AuditLog events={events} now={now} timeZone="UTC" pageSize={6} className="max-w-3xl" actions={ <Button variant="outline" size="sm"> <DownloadIcon data-icon="inline-start" /> Export </Button> } /> )}Anatomy
<AuditLog events={events} onLoadMore={loadOlder} hasMore={cursor !== null} timeZone="UTC" actions={<Button variant="outline" size="sm">Export</Button>}/>Guidance
Use it when
- Security, compliance and admin pages: role changes, key creation, exports, sign ins and settings.
- A record's own history, filtered to that resource.
- Agent actions, with the approval that allowed them in metadata.
Avoid
- Activity feeds for collaboration. Use Timeline with comments and attachments.
- Application logs and errors. They need levels and streaming, not actors and records.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Map each server event to { id, action, actor, resource, at, changes?, ip?, location?, userAgent?, metadata?, severity? }. Keep action dotted, like invoice.updated.
- The sentence is built from actor, the last part of action and the resource; pass summary only when that reads wrong.
- Record changes as { field, before, after } with display values, not raw ids.
- For large logs, page on the server: pass onLoadMore and hasMore and append events. Otherwise pageSize reveals more rows locally.
- Pass timeZone="UTC" for compliance views and a fixed now in tests so relative ranges stay stable.
- Mark security relevant events with severity warning or critical. Never put secrets in metadata.
API
AuditLog
| Prop | Type | Default | Description |
|---|---|---|---|
| events | AuditEvent[] | Events in any order; they sort newest first. | |
| pageSize | number | 25 | Rows before Show more, when everything is loaded. |
| onLoadMore / hasMore | () => Promise<void> / boolean | Server paging. Load older events calls onLoadMore. | |
| loading | boolean | false | Skeleton rows. |
| now / timeZone | Date / string | The clock for ranges and the zone for times and days. | |
| actions | ReactNode | Right side of the toolbar, like Export. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add audit-log# or, with the registry in components.jsonnpx shadcn@latest add @prodular/audit-log