Skip to main content

Audit API

The audit router provides read access to the tenant's audit log. All mutations across the platform are automatically recorded, creating a compliance-ready trail of changes. Access is restricted to admin users.

Router namespace: audit

Source: src/server/trpc/routers/audit.ts

Access: All procedures require admin role.

Procedures

ProcedureTypeAccessDescription
listqueryadminPaginated audit log with cursor-based pagination
getByResourcequeryadminFilter audit entries by resource type and optional ID

list

Returns a paginated list of audit log entries ordered by most recent first. Uses cursor-based pagination for efficient traversal of large logs.

audit.list.queryOptions(input)

Input

FieldTypeRequiredDefaultDescription
limitnumber (1-100)No50Maximum entries per page
cursorstringNo--ISO 8601 timestamp cursor from previous page

Response

{
"items": [
{
"id": "aud_abc123",
"tenantId": "tenant_789",
"userId": "user_001",
"action": "settings_change",
"resource": "scoring_config",
"resourceId": null,
"metadata": {
"configType": "combined",
"version": 3
},
"createdAt": "2026-03-05T10:30:00.000Z"
},
{
"id": "aud_def456",
"tenantId": "tenant_789",
"userId": "user_002",
"action": "connector_sync",
"resource": "connector",
"resourceId": "conn_456",
"metadata": {
"syncType": "incremental",
"recordsCreated": 150
},
"createdAt": "2026-03-05T08:00:00.000Z"
}
],
"nextCursor": "2026-03-05T08:00:00.000Z"
}

When nextCursor is null, there are no more pages.

Cursor-Based Pagination

Unlike offset-based pagination, cursor pagination provides consistent results even as new entries are added. Pass the nextCursor value from the previous response as the cursor input for the next page:

const trpc = useTRPC();

// First page
const page1 = useQuery(
trpc.audit.list.queryOptions({ limit: 25 })
);

// Next page (when user clicks "Load More")
const page2 = useQuery(
trpc.audit.list.queryOptions({
limit: 25,
cursor: page1.data?.nextCursor ?? undefined,
}),
{ enabled: !!page1.data?.nextCursor }
);
Infinite Scrolling

Use TanStack Query's useInfiniteQuery for infinite scroll patterns with the cursor from nextCursor.


getByResource

Filters audit log entries by resource type and optionally by a specific resource ID. Returns up to 100 entries ordered by most recent first.

audit.getByResource.queryOptions(input)

Input

FieldTypeRequiredDescription
resourcestringYesResource type (e.g., "scoring_config", "connector")
resourceIdstringNoSpecific resource identifier

Response

Returns the same item structure as list, without pagination:

[
{
"id": "aud_abc123",
"tenantId": "tenant_789",
"userId": "user_001",
"action": "settings_change",
"resource": "scoring_config",
"resourceId": null,
"metadata": {
"configType": "fit",
"version": 2
},
"createdAt": "2026-03-05T10:30:00.000Z"
}
]

Example

const trpc = useTRPC();

// All scoring config changes
const { data: configChanges } = useQuery(
trpc.audit.getByResource.queryOptions({
resource: "scoring_config",
})
);

// Changes for a specific connector
const { data: connectorHistory } = useQuery(
trpc.audit.getByResource.queryOptions({
resource: "connector",
resourceId: "conn_456",
})
);

Audit Event Structure

Every audit log entry contains these fields:

FieldTypeDescription
idstringUnique audit entry identifier
tenantIdstringTenant scope
userIdstringClerk user ID who performed the action
actionstringAction type (see table below)
resourcestringResource type affected
resourceIdstring | nullSpecific resource identifier (if applicable)
metadataRecord<string, unknown>Action-specific details
createdAtDateWhen the event occurred

Common Action Types

ActionResourceDescription
settings_changescoring_configScoring configuration updated
connector_syncconnectorData sync triggered
connector_disconnectconnectorConnector disconnected
writeback_triggerwritebackScore writeback initiated
role_confirmedbuying_group_memberBuying group role manually confirmed
settings_changetenantTenant settings updated
Automatic Logging

Audit entries are created automatically by tRPC middleware on all mutations. You do not need to manually log actions -- the middleware handles this.