> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getpostern.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The audit log

> What one row holds, what gets a row, and what does not.

<Info>
  **Before you start**

  * **You can read this only on the machine Postern runs on.** From your own
    computer, forward the port first, then open `http://localhost:8787`:

    ```bash theme={"system"}
    ssh -L 8787:localhost:8787 you@the-machine
    ```
  * **It grows forever.** Nothing in Postern deletes a row and there is no
    retention job. You must trim it, back it up and provide the disk yourself.
  * **There is nothing to switch on.** The migrations create the `audit_log` table
    at first boot, and the writer starts with Postern. No setting turns it off.
</Info>

Three names, one thing. **The record** is the history of agent calls. **The
ledger** is the Console screen that shows it. `audit_log` is the table that holds
it. You need no agent key to read it: whether you can reach this computer is the
whole of the authorization.

**The record tells you what your agents did. It does not tell you what you did to
your agents.** Six acts leave no row at all: create a key, rotate one, edit its
sectors or its deadline, revoke it, delete it, and set a connection's sync window.
Postern records everything else on this page. And nothing here stops anything:
the record is a witness, never a guard.

Two more pages carry the rest of the record.
[Read the record](/reference/read-the-record) is the ledger screen and
`GET /api/audit` — the filters, the paging and **Export CSV**.
[What the record cannot promise](/reference/record-limits) is how far it can be
trusted — what is dropped before it lands, what nothing in the database prevents,
and the five questions it cannot answer.

## What one row holds

One row is one recorded call. Postern writes rows in batches with a single
`INSERT`. It binds every value as a parameter, so no argument an agent sent ever
reaches SQL as text.

| Column         | Type          | What it holds                                                                                                                                                                                                           |
| -------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`           | `bigserial`   | Primary key, and the tiebreak that makes pages exact. Every row in one flush shares a `created_at` to the microsecond.                                                                                                  |
| `user_id`      | `uuid`        | References `users(id)`; null when no user resolved. **Not served over the API, not shown in the Console.**                                                                                                              |
| `agent_id`     | `text`        | The agent's own name; `operator` for a Console action; null for a refused handshake and a device push. Plain text, no foreign key.                                                                                      |
| `tool`         | `text`        | The verb. Not null, free text. See below.                                                                                                                                                                               |
| `sector`       | `text`        | The sector the call happened in, or null.                                                                                                                                                                               |
| `decision`     | `text`        | `allow` or `deny`, only ever those two. Not null.                                                                                                                                                                       |
| `deny_reason`  | `text`        | `unknown agent`, `revoked` or `expired` when Postern refused the key. Only the agent port ever writes those three. `access denied: <sector>` when the key was good and the grant was not; both surfaces write that one. |
| `args`         | `jsonb`       | The call's parameters, redacted and size-capped before Postern queued them.                                                                                                                                             |
| `result_count` | `int`         | Rows returned on a read. On a device push, records upserted plus deleted.                                                                                                                                               |
| `outcome`      | `text`        | `ok` or `error`. Actions record what the connector reported; reads record `ok`.                                                                                                                                         |
| `request_id`   | `text`        | The MCP request id, to line a row up against a client's own log.                                                                                                                                                        |
| `created_at`   | `timestamptz` | Set by the database at insert time.                                                                                                                                                                                     |

One index: `(user_id, created_at desc)`.

`created_at` is when the row was **inserted**, not when the call happened. Postern
flushes every 1000 ms by default, so a stamp trails its call by up to that much.
Every row in one flush carries the same instant, and under load the gap widens.
Nothing records the call's own time.

## What gets a row

**Agent calls — all seven tools.** `describe_context`, `get_schema`, `query`,
`get_record`, `fetch_live`, `list_actions`, `invoke_action`. The agent port writes
a row for every one, served or refused. The REST read mirror carries the five
reads, and writes the same row for those. The agent key is never in any field, on
any branch.

That covers a **sector** refusal on both surfaces. A refused **key** is the one
asymmetry, and [what does not get a row](#what-does-not-get-a-row) has it.

A refusal writes `decision: deny` with the reason. A call Postern allowed, which
then failed at the work, writes `decision: allow` with `outcome: error`. The
Console paints that third state as **Errored**.

**Console actions.** These carry `agent_id` of `operator`, always write
`decision: allow`, and carry `outcome` of `ok` or `error`.

| Verb                                                | What it was                                                                                                                       |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `pause` · `resume` · `sync_now` · `disconnect`      | You paused, resumed, synced or removed a connection                                                                               |
| `revoke`                                            | You revoked **a source's credential** — not an agent key                                                                          |
| `set_sync_interval`                                 | You changed how often Postern checks a source                                                                                     |
| `set_plaid_app` · `delete_plaid_app`                | You set or removed your own Plaid credentials                                                                                     |
| `set_exposure` · `clear_exposure`                   | You set or cleared the public address                                                                                             |
| `finance.dedup.confirm` · `finance.dedup.unconfirm` | You merged or unmerged two finance rows                                                                                           |
| `export`                                            | Carries the sector if you narrowed it, and the row count — never the rows                                                         |
| `erase`                                             | Carries `{scope, connectionId?, sector?}` and the number of rows deleted. A failed erase is recorded too, with `outcome: "error"` |

**The handshake and the bridge.**

* `initialize` — Postern writes this **only on a refusal**, with `agent_id` null.
  It always writes a key that was presented and refused. It writes an anonymous
  probe that carries no key only when the OAuth bridge is on. A successful session
  start writes nothing; the first tool call is what appears.
* `oauth_token` — the bridge issued or refreshed a token for a hosted agent. An
  allowed row carries `{ grant: "authorization_code" | "refresh_token", clientId }`.
* `ingest` — a device pushed records over a webhook. `agent_id` is null; an
  unauthorized push writes `deny` with `unauthorized`.

Postern caps two kinds of refusal on the agent port, each at 30 a minute. Past the
cap it counts them rather than records them. Refused `initialize` calls get the
summary row `throttled: <n> initialize denials coalesced`. The bridge's cheap
refusals — an unknown client, an address that does not match, a stale form — get
`throttled: <n> further bridge denials suppressed`. Both land at the **start of the
next window, and only if another refusal arrives**. So if the flood stops, the
count goes with it. Under a flood, "no row" no longer means "nothing arrived".

**A refused agent key is never one of the counted ones.** At the sign-in page the
bridge records every one, at any rate: `authorize: key refused`. A key that never
expires gets `authorize: key has no expiry`. A rate you would suppress is the
alarm itself.

## What does not get a row

<Warning>
  Six Console acts write no row at all: create a key, rotate one, edit an agent's
  sectors or its deadline, revoke the agent, delete the agent, and set a
  connection's sync window. Five of the six are agent-key management — the acts
  you would most want a record of. Keep your own note.

  Four of those five end every live session that agent holds: a rotate, an edit to
  its sectors or its deadline, a revoke and a remove. So an agent can stop
  mid-work with nothing in the record to say why.
  [What each verb does](/reference/agent-keys#revoking-rotating-and-removing).
</Warning>

Two traps in that list. The `revoke` verb above is **a source credential**, not an
agent. And `set_sync_interval` is a different control from the sync window, and
**is** recorded.

**Postern does not record reads you make yourself.** The Console's data inspector
is `POST /api/data/:sector` and `GET /api/data/:sector/:id`. It runs the same
query engine an agent runs, as the operator with every sector granted, and writes
nothing. Nor do `GET /api/operator/context`, `GET /api/operator/schema`, reads of
the record itself, or **Export CSV**.
[Why reachability here counts as operator trust](/reference/ports#the-console-has-no-authentication).

**On the REST read mirror, a refused key leaves no row.** That covers both
refusals it can give: a missing or malformed key (`401`), and an unknown, revoked
or expired key (`403`). The handler resolves the key first and writes the row
second, so a key that never resolves fails before the write runs. An ungranted
*sector* is different: the key resolved, so Postern records that refusal.

**On the agent port, the same refused key does leave a row** — against the tool
`initialize`, with the reason under `deny_reason`, no agent name, and no sector. A
reason never travels in the sector column, so a sector filter never shows that
row. One surface remembers a bad key and the other does not.
[The two refusals](/reference/refusals#the-two-refusals).

The line falls where a call touches a source or changes state, not where it only
reads. Postern records an operator `fetch_live`, because it reaches a source and
changes the cache. It records `export` too, which returns the whole store.

<Warning>
  The record is not a list of everything that has read your data. You can read
  every row of every sector through the Console's inspector and leave no trace. So
  can anything else on that machine that can reach the Console port. What the
  record answers completely is which **agent** came through and what it asked for.
  What it cannot answer is who has seen this data.
</Warning>

## What the arguments hold

`args` is the call's own parameters — the query, the action target and its values
— stored as JSON. Two defences run before Postern queues the entry.

* **Field names.** Postern replaces any value whose field name contains `pass`,
  `secret`, `token`, `key`, `authorization` or `cookie` with `[redacted]`,
  matched without regard to case. It recurses through nested objects and arrays.
* **Size.** Past roughly 4 KB serialized, the whole payload collapses to
  `[truncated]`. So does a cyclic or non-serializable value.

The honest bound: **redaction matches field names, never values.** Put a secret in
a value under a name the pattern does not match, and Postern records it as it
arrived. Secrets are not meant to travel in tool arguments. This list is a second
line, not the first.

The record holds the act, never the row values. `result_count` is a count; what a
read returned is not in the table.

The Console renders `args` two ways. The table drops fields whose value is null.
The row detail panel prints them whole under **Args (redacted, verbatim)**, and
**Export CSV** carries them exactly as recorded.

## What you have now

A record of every agent call and of the Console verbs listed above. It does not
cover agent-key management, and it does not cover reads you make yourself.

## Next

<Columns cols={2}>
  <Card title="Read the record" href="/reference/read-the-record">
    the ledger screen and `GET /api/audit` — filters, paging, and Export CSV
  </Card>

  <Card title="What the record cannot promise" href="/reference/record-limits">
    what is dropped before it lands, and what nothing in the database prevents
  </Card>

  <Card title="Connect an agent" href="/start/connect-an-agent">
    a running Postern and an agent key · a few minutes, plus a restart of your
    client · a key shown once
  </Card>

  <Card title="Grants and sectors" href="/reference/grants-and-sectors">
    what a grant covers, and how to revoke, rotate or expire one
  </Card>
</Columns>
