
The audit stream shows what the platform actually did after a client
action. This guide shows how to tail it and how to read a record. For the full
catalog of actions, attributes, and evidence tiers, see
[Audit events](/reference/audit-events/).

## Where each plane stores the audit

Each plane serves the same two read routes over its own audit store:

- `GET /api/v1/audit/events`: a filtered, paginated read.
- `GET /api/v1/audit/stream`: a Server-Sent-Events live tail with the same
  filters.

The enclave broker serves the sovereign enclave audit. Reach it through the
relay (`:8452`), which proxies both routes, or at the broker itself (`:8451`).
The controlplane serves its own identity and policy audit. The `--source` flag
selects the plane: `enclave` (the default) or `controlplane`. There is no merge:
a query reads one plane's store.

## Tail the stream

`dvx audit stream` prints one JSON record per line until you interrupt it.
Authenticate with `--token`, or log in once with `dvx login` so the CLI can
reuse the cached device-flow credential. Point TLS trust at the platform's CA
bundle with `--ca-cert` (`--insecure-skip-verify` exists but is dev-only).

{{< terminal title="dvx" prompt="$" >}}
$ dvx audit stream \
    --source enclave \
    --enclave https://relay.enclave.example.com:8452 \
    --ca-cert ca.pem \
    --action gateway.egress
{"event_time":"...","action":"gateway.egress","outcome":"deny",...,"source":"network_flow"}
{{< /terminal >}}

Both commands take the same filter flags: `--scope`, `--agent`, `--action`,
`--outcome`, `--proto`, `--dst-host`, `--since`, and `--until` (RFC3339).
`dvx audit events` adds `--limit` (default 100, at most 1000) and `--offset`
for paging. Use `events` when you want a bounded, exact answer and `stream`
when you want rows as they happen.

For a raw client (a dashboard, a log shipper, `curl`), the stream is plain SSE:
each event carries an `id:` line and one JSON record on the `data:` line, and
the server writes `: keepalive` while the stream is idle. The id is the row's
ingest time in unix nanoseconds. Send it back as `Last-Event-ID` on reconnect
and the tail resumes exactly after that row.

The server does not keep a tail connection open forever: it bounds each
response with a write deadline and closes the connection after about 30
seconds. A long watch
reconnects in a loop with the last id. When the close arrives as a read error,
`dvx audit stream` prints the error and exits.

## What a record looks like

Both routes return the same unified record. One timeline covers all of a
plane's audit: generic audit events, typed network-flow records, and typed
inference records. The `source` field tells you which store the row came from,
and each source fills a different set of fields:

- `network_flow`: a gateway egress flow. Fills the network fields (`proto`,
  `src`, `dst`, `dst_host`, `identified`, `agent_name`, and the narrower
  address fields like `dst_ip` and `dst_port`) and leaves `resource`
  and `attrs` empty.
- `audit_event`: a generic audit action. Fills `resource` and `attrs` and
  leaves the network fields empty.
- The unified view projects each typed inference record as action
  `inference.request` with its metadata in `attrs`. The record's body
  ciphertext never appears in the timeline.

The shared correlation fields are present on every row: `event_time`, `action`,
`actor`, `actor_kind`, `outcome`, `reason`, `scope`, `org_id`, `group_id`,
`agent_id`, `session_id`, `trace_id`, and `enclave_id`.

{{< terminal title="dvx" prompt="$" >}}
$ dvx audit events --enclave https://relay.enclave.example.com:8452 \
    --ca-cert ca.pem --action gateway.egress --outcome deny --limit 1
{
  "records": [
    {
      "event_time": "2026-10-08T12:16:46.035Z",
      "enclave_id": "c359fcbd890f...",
      "enclave_name": "daevix-dev",
      "scope": "agent",
      "org_id": "dev-org",
      "agent_id": "egressdoc-verify",
      "action": "gateway.egress",
      "actor": "agent:egressdoc-verify",
      "outcome": "deny",
      "reason": "default_deny",
      "identified": true,
      "agent_name": "egressdoc-verify",
      "src": "10.244.1.192:47522",
      "dst": "146.75.121.55:443",
      "dst_host": "www.kernel.org",
      "dst_ip": "146.75.121.55",
      "dst_port": 443,
      "proto": "tcp",
      "source": "network_flow"
    }
  ],
  "next_offset": 1
}
{{< /terminal >}}

The same record arrives as one `data:` line on the stream.

## Correlate a client action with server-side evidence

The read is fail-closed in both directions. Every query is force-scoped to the
caller's org, taken from the token, never from the request. A caller without an
org claim gets `403`. On the enclave, the broker resolves the caller's
enclave-local grants: `audit:read:all` sees every actor in the org, and
`audit:read` pins the read to records attributed to the caller's own actor. On
the controlplane, the same personas resolve from capabilities. A caller can
narrow with filters but can never widen past the computed scope.

Correlation then works like this:

1. Trigger the action from the client (write a secret, author a policy, run an
   agent against the gateway).
2. Watch the stream (or page `events`) for the row with the action and outcome
   that you expect, matched by `trace_id`, `agent_id`, or `request_id` where the
   action carries one.
3. On the controlplane, a cross-org read is itself audited: each affected org
   gets an `audit.read.cross_org` event in its own view, and a paged read
   refuses to disclose its results if that event cannot be durably recorded.

The stream is a best-effort tail: the `events` endpoint is the source of truth
for exact and complete queries. Each component that writes to an audit store
also emits `audit.heartbeat` records on a fixed interval, and the store emits
`audit.gap` records when it lost rows, so a reader can tell silence from loss. See
[Author an egress policy](/guides/author-an-egress-policy/) for an example of
reading the deny verdicts that a policy produces, and
[Operator API](/reference/operator-api/) for the authentication model these
routes share.
