Skip to content
Metric VaultHelp Center
Open app

Audit log

The complete list of the 17 audited admin actions, what each row records, how to search and export the log, and what is deliberately not recorded.

Last updated 2026-08-06

Summary#

Every mutation made through an /api/admin/* endpoint writes one row to the admin audit log: who did it, what they did, what they did it to, and a short detail string. Seventeen distinct actions exist and they are listed in full below. Reads are not recorded, and two mutations sit outside the audited path. The log is exposed on the System Health screen and on the Customers screen, and it can be exported to CSV.

Overview#

The audit log answers "who changed this, and when" for the staff console. It is written by a single helper that every admin mutation calls after its work succeeds. That write is best-effort: if it fails, the action it was recording still stands. The design choice is deliberate. An audit failure must never roll back a customer fix, so the log is a record of intent rather than a transactional guarantee.

The table holds five fields plus an auto-incrementing id.

FieldTypeMeaning
idintegerAuto-increment. The log is ordered by this, newest first
tsintegerUnix seconds, not milliseconds
actortextThe verified admin email that made the change
actiontextOne of the seventeen values below
targettextWhat was changed. Usually an email, a key or an id
metatextA short detail string, or empty

actor is the email the worker resolved from the caller's token, not a value the browser supplied, so it cannot be spoofed by editing the request body.

The 17 audited actions#

Seventeen places in the worker write an audit row. Every one of them writes it after the change has been applied.

#actionWritten bytargetmetaRole needed
1issue.resolved or issue.unresolvedResolving or reopening an error issue. The status becomes part of the action nameIssue fingerprintnoneadmin
2blog.statusModerating a customer postPost idThe new statusowner
3config.deleteDeleting a configuration keyThe keynoneowner
4config.setSaving a configuration keyThe keyThe value, first 120 charactersowner
5jobs.runPressing Run due nowThe job keynoneowner
6cache.clearClearing cached provider dataTool id, or all"<n> rows"owner
7cache.setallApplying one lifetime to every tool"<n> days"noneowner
8social.disconnectRevoking a customer's social channelCustomer emailPlatform, or platform:accountIdowner
9customer.set_planChanging a customer's planCustomer emailThe planowner
10customer.reset_usageZeroing this month's usageCustomer emailThe month keyowner
11customer.refund_creditsRefunding creditsCustomer emailThe amountowner
12customer.suspendSuspending a customerCustomer emailnoneowner
13customer.reactivateLifting a suspensionCustomer emailnoneowner
14customer.remove_seatRemoving a team memberCustomer (owner) emailThe member emailowner
15admin.addAdding an adminAdmin emailrole=<role>owner
16admin.updateSuspending, activating, promoting or demoting an adminAdmin emailrole=<r or -> status=<s or ->owner
17admin.removeRemoving an adminAdmin emailnoneowner

That is seventeen call sites and eighteen possible action strings, because row 1 writes either issue.resolved or issue.unresolved depending on which button was pressed. When you filter by action, customer. matches rows 9 to 14, admin. matches 15 to 17, and config. matches 3 and 4.

Note

Note: config.set stores the first 120 characters of the value it wrote. That is a convenience for reading history, not a secret store. Do not put credentials in configuration values.

What is not audited#

Not recordedWhy it matters
Every read endpointLoading a customer's record, exporting subscribers, opening the Run Log and reading contact messages leave no trace. There is no record of who looked at what
Resolving a workflow failureThe endpoint sits outside /api/admin/* and writes no audit row. It does set resolved_at, so the time survives but the actor does not
Anything done in Messages or Bug ReportsThose screens talk to Supabase directly from the browser. Marking read, marking responded and deleting are unaudited
Sign-in and sign-outAuthentication events live in Supabase, not here. The admin_users row does carry a last_seen_at stamp updated on every verified request
Changes made outside the consoleA direct database write, an environment-variable change or a deploy produces no row

Reading the log#

On System Health#

Admin console → Operations → System Health → Recent admin actions

ControlBehavior
Header count(<loaded> of <total>). The total respects the active filters
actor fieldSubstring match, case-insensitive. Press Enter or Filter
action fieldSubstring match. customer. finds every customer mutation
FilterRe-runs from the first page with the current filters
Load moreFetches the next 50 rows. Hidden once everything is loaded
Export CSVWrites the loaded rows to metricvault-audit-<date>.csv
Columns`WhenActorActionTarget, with meta` appended after the target
Empty stateNo admin actions recorded.

The CSV header is When (unix),Actor,Action,Target,Meta. The When column is raw unix seconds rather than a formatted date, so a spreadsheet needs a conversion before it reads as a timestamp.

Screenshot
The Recent admin actions panel on System Health with the actor and action filters populated and Load more visible.

On a customer record#

Admin console → Operations → Customers → Recent actions on this customer

Loading a customer also queries the audit log filtered to that email as the target, most recent 25 rows. It is the fastest way to answer "has anyone touched this account". The empty state is No recorded admin actions on this customer.

Because the filter matches the target column, actions where the customer is the target appear (plan changes, refunds, suspensions, seat removals, social disconnects). Actions where their email only appears in meta, such as a seat removal recorded against a workspace owner, are found by searching the owner instead.

Query limits#

ParameterDefaultBounds
limit501 to 500
offset00 or greater
actor filternoneSubstring, lower-cased before matching
action filternoneSubstring, case-sensitive
target filternoneSubstring, lower-cased before matching
since filternoneUnix seconds, matched against ts

Rows are always returned newest first by id. The console's own page size is 50.

Retention and deletion#

Nothing prunes this table. There is no retention window, no cleanup job, and no endpoint that deletes audit rows. The console cannot clear its own history, which is the property you want from an audit trail. Growth is bounded in practice by the fact that only mutations are recorded, and the console is used by a small number of staff.

If the log ever needs trimming, it has to be done directly against the database by someone with that access. Doing so leaves no record, by definition.

Failure behavior#

SituationResult
The audit write failsThe action it recorded still succeeded. No error is surfaced
MONITOR_DB is unboundNothing is recorded, and every admin screen is already failing for the same reason
Two admins act at the same timeBoth rows are written. The auto-increment id orders them
An action affects zero rowsIt is still audited. A refund against a customer with no usage row records customer.refund_credits and reports success

That last row is worth remembering during an investigation: an audit entry proves somebody pressed the button, not that anything changed. Confirm the effect on the customer record itself. See Customer lookup and billing.

See also

Was this article helpful?