Every admin mutation in TeleQuick writes a row to audit_log. The console’s Audit log screen is a thin reader over two tRPC queries: adminAudit.list for the entries and adminAudit.options for the filter vocabularies. Both are scoped to a single org, so a caller only ever sees the audit trail of the org it passes as orgId.

What gets written and when

An entry is appended when an admin mutation completes. The write is a side effect of the mutation, not something you trigger — there is no “create audit entry” procedure in this surface. The action string on the row identifies the mutation that produced it, and resource_type plus resource_id identify what it touched. Entries fall into two shapes:
  • Resource mutations — a resource_id is set, and before / after carry the JSON state of that resource on either side of the change.
  • Events without a single target — resource_id is null. Bulk operations and session events look like this.

The audit entry record

adminAudit.list returns rows, an array of records with the following fields, plus total, the count of entries matching the filters (not the count returned on this page). before and after are opaque JSON. Their shape follows the resource they describe, so do not assume a common schema across resource_type values.

Action and resource type vocabulary

Neither vocabulary is a fixed enum. Both are derived from the entries that actually exist in the org, and the console fetches them from adminAudit.options:
Build filter UIs from this call rather than hardcoding a list — an org that has never deactivated anything will not have a deactivation action in its vocabulary. Action strings follow verb conventions that the console relies on to colour each timeline dot. Severity is not stored on the row; it is inferred client-side from the action string, which is the source of truth: If you write your own reader, reproduce this mapping or ignore it — the row carries no severity column either way.

Actors, IP and request id

actor_user_id is the acting user. When a mutation originates from the platform rather than a person, it is null; the console renders those entries as system. actor_label is a denormalised display name captured at write time, so it survives later renames or user deletion — prefer it for display and fall back to actor_user_id, then system:
ip is the source address of the request that caused the write, and is shown inline next to the actor. request_id is on the record but the console timeline does not render it; use it to join an audit entry back to application logs and traces for the same request.

Reading before and after diffs

Expanding an entry renders a JSON diff of before against after. The console only renders the diff panel when at least one of the two is non-null, so entries that carry neither expand to nothing. Read the two fields together to classify the change: Both values are stored as JSON, so over the API you can diff them with whatever structural diff library you already use.

Filtering, search and paging

adminAudit.list accepts these inputs. Every filter is optional except orgId, and omitting one means “any”. The console’s Range chip is a convenience over since: Today sends local midnight, Last 7 days and Last 30 days send now minus that many days, and All time omits since entirely. Paging is page-number based. total is the number of entries matching the filters across all pages, so compare it against the rows you already hold to decide whether another page exists:
Changing any filter resets the page index back to 1. Keep the previous page’s rows on screen while the next query is in flight if you want to avoid a flash of empty state — the console does this with keepPreviousData. The console caches the adminAudit.options response for 60 seconds, so a brand-new resource_type may take that long to appear in the filter dropdowns after its first entry is written. Query options directly if you need it fresh.

Aggregating a filtered window

The console’s right-hand rail — top actors and action mix — is computed in the browser from the rows currently loaded, not from the whole filtered range. That is why it shows Loaded and Total in range as separate numbers. If you need aggregates over an entire range, page through list yourself and aggregate the full result; do not read a single page’s distribution as representative of the range.

Immutability and retention

Audit entries are append-only. The surface behind this screen exposes adminAudit.list and adminAudit.options, both read-only queries — there is no procedure to edit, redact or delete an entry, and the console offers no such control. Treat a written row as final, including its before / after payloads. Because entries are immutable, the practical bound on how far back you can read is whatever the org’s audit data retention allows. Use since: undefined (the All time range) to read the oldest entries still available, and export anything you need to keep beyond that outside the platform.
  • Authentication — API keys and the scopes that gate control-plane mutations
  • Telemetry — traces carry the same request correlation ids you see on audit entries