The TeleQuick console writes an audit row every time an operator or a service changes a tenant-scoped resource. The rows live in the audit_log table, scoped to one org, and the console reads them back through tRPC. This page explains what each field means, what the action vocabulary covers, and how to page through the log outside the console. Two procedures expose the same table:

What gets recorded

An entry is written for a change to a tenant-scoped resource: the insert, update or delete of a configuration row, a hydration of Postgres state into the runtime cache, and credential writes and removals. Each entry names the actor, the action, the resource it touched, and — where the writer supplied them — the resource state before and after the change. The log is per-org. admin.auditList requires an orgId and filters on org_id; access is additionally constrained server-side by row-level security, so a caller only ever reads rows for orgs it is a member of. Rows are returned newest-first (admin.auditList orders by ts descending).

Anatomy of an entry

admin.auditList selects these columns and returns them unmodified: The response envelope is:
total is an exact count of the rows matching the current filters, not the number returned in this page.

Actors: users and the system fallback

Three fields describe who acted, and they are populated independently:
  • actor_user_id — set when a console user performed the change.
  • actor_label — a display string for the actor. Prefer it in your UI; it is what the console shows.
  • Neither set — the change did not originate from a signed-in user. Both console screens fall back to rendering the actor as system in that case.
The portal resolves the actor column as actor_label, then a shortened actor_user_id, then system. Reproduce that order if you render the log yourself, because a row can carry a label without a user id, or a user id without a label. adminAudit.options returns an actors array of { id, label } for the org, built from the actors that actually appear in the log. Feed its id values back as actorUserId to filter. Rows with no actor_user_id cannot be selected that way — the system actor has no id to filter on.

Resource types

The portal’s resource filter offers this fixed set: The filter is an exact-match equality on resource_type, so a value must be spelled exactly as it was written. The Voice AI screen instead populates its dropdown from adminAudit.options.resourceTypes, which reflects the resource types that are actually present in that org’s log — use that if you want a list that tracks the data rather than a hardcoded vocabulary.

Actions

hydrate entries are not user edits in the usual sense — they record that the source-of-truth rows were re-published to the runtime. Trunk transfer rules work this way: Postgres holds the priority-ordered list, and every write re-fetches the full list for the trunk and rewrites the Redis key that the runtime’s transfer tool reads. So a single rule edit can produce both a write action and a hydrate action. Credential writes get their own two actions rather than being folded into insert / update / delete, which lets you filter for credential activity on its own. The portal colour-codes any action containing credential as a warning. Action colours in the portal table: The action filter is also an exact equality match. The portal’s dropdown lists the six values above; adminAudit.options.actions returns the distinct actions present in the org’s log, so an action written by a newer service shows up there before it is added to any hardcoded list.

Before and after payloads

before and after are opaque JSON columns. The procedures select them directly and return them as stored — no shaping, diffing, filtering or masking happens in the read path. What you get is exactly what the writer of the row recorded. Consequences worth planning for:
  • Either side can be absent. Both console screens only render a payload when it is non-null, and the Voice AI timeline only makes an entry expandable when at least one of the two is present. An insert typically has no meaningful before; a delete typically has no after; some actions record neither.
  • The shape is per-writer. There is no common envelope across resource types. Do not assume a field exists in after because it existed for another resource type.
  • Treat the payloads as sensitive. vendor_creds is an audited resource type, and the read path performs no redaction of its own. If you expose the log in your own surface, apply whatever masking your policy requires at that layer rather than relying on the API to have removed anything.
Both console screens render the payloads as pretty-printed JSON side by side rather than computing a structural diff.

Request context: IP, user agent and request ID

ip, user_agent and request_id describe the request that caused the change, and are each independently nullable — a change made by a non-interactive actor may carry none of them. request_id is the most useful of the three for investigation: it is the id of the control-plane request that produced the row, so it is the value to line up against your own request logs when you need the full context around a change. The portal shows all three in the detail panel; the Voice AI timeline shows ip inline next to the actor.

Retention and immutability

The log is append-only. Rows are written once and are never edited or deleted through these surfaces — there is no update or delete procedure for audit_log, and the console states this on the page itself. A correction therefore appears as a new entry, not as a modification of the original. Neither procedure applies an expiry or trims old rows, and neither exposes a retention setting. The only bound in the read path is the time filter you pass: adminAudit.list accepts a since timestamp, and the Voice AI screen’s All window computes since from a 3650-day lookback rather than omitting the filter. If you need a guaranteed retention horizon for a compliance requirement, confirm it against your contract — it is not expressed in the API.

Reading the log over the API

The log is readable outside the console through the same tRPC procedures the console calls.
Narrow by resource type and action:
admin.auditList input: The Voice AI variant takes a different shape, including free-text search and an actor filter:
Both return { rows, total }. If the underlying query fails, admin.auditList throws a tRPC INTERNAL_SERVER_ERROR carrying the database error message. Because orgId is required and the query is additionally scoped by row-level security, there is no cross-org read: to audit several orgs, call once per org.

Filtering and pagination

  • Empty means unfiltered. The console maps its “All resources” / “All actions” selections to omitting the field entirely. Send undefined, not an empty string, for a filter you do not want applied.
  • Filters combine with AND. resourceType and action narrow the same query; total reflects the filtered count, so it changes as you change filters.
  • Reset the page when a filter changes. Both screens set the page back to the first page whenever a filter changes, because an offset that was valid for the wider result set usually points past the end of the narrower one.
  • Offset vs page. admin.auditList is offset-based (offset = page * limit); adminAudit.list is 1-based page numbering. Do not mix the two conventions.
  • Page count. With admin.auditList, derive the number of pages from total and your limit; the exact count makes that reliable without a separate count call.
  • Maximum page size is 500. Requesting more is rejected by input validation. For a full export, iterate offset rather than raising limit.
Time-bounded reads are only available on adminAudit.list, via since. When you compute that timestamp on the client, memoise it — deriving it from a bare Date.now() on every render changes the query key each time and the query refetches continuously.
  • Authentication — API keys and relay tokens; the credentials whose use these entries record
  • Telemetry — metrics, traces and CDRs, which cover call traffic rather than admin actions