# Session recordings

> How session recordings reach the store, what the recording index holds, how playback links are minted, and how to list and fetch recordings over the API.

The Recordings screen in the ClutchCall QuickDesk console is a view over
two separate artifacts:

| Artifact | Where it lives | What it holds |
| -------- | -------------- | ------------- |
| Recording row | The `quickdesk_recording` index | Metadata: file name, peer, size, timestamp, storage key |
| Recording object | Object store (MinIO), encrypted at rest | The MP4 bytes |

The list you see is the index. The bytes are never streamed through the
console — the console mints a link to the object store instead and opens
it in a new tab.

## How a recording gets into the store

A recording appears because **a client uploaded it**. The screen has no
start, stop, or record control, and neither the list query nor the
playback mutation writes bytes. Until a client uploads something, the
list is empty and the screen renders the "No recordings yet" state.

Each upload produces one index row plus one object. The row's
`storage_key` is the only handle that ties them together — it is the
value the console passes back when you press **Play**.

## The recording index and its fields

`adminQuickdesk.recordings.list` returns `{ recordings, total }`. Each
entry in `recordings` carries the fields below. The right-hand column is
how the console renders them.

| Field | Rendered as | Notes |
| ----- | ----------- | ----- |
| `guid` | (not shown) | Row identity. Used as the React key. |
| `file` | **File** | The recording's file name. Truncated with an ellipsis in the table; the full value is in the API response. |
| `peer_id` | **Peer** | The remote peer for the session. May be empty — the console shows `—`. |
| `size` | **Size** | Byte count, coerced with `Number()` before formatting, so treat it as numeric-but-not-necessarily-a-JS-`number` on the wire. |
| `created_at` | **Recorded** | When the recording was created, formatted as a relative/absolute timestamp. |
| `storage_key` | (not shown) | The object-store key. Required input to `playbackUrl`. |

`total` is the count of rows in the index, independent of the page you
requested. The console prints it in the page subtitle.

## Playback links and how long they last

Pressing **Play** calls `adminQuickdesk.recordings.playbackUrl` with the
row's `storage_key`. The mutation returns `{ url }` — a **presigned
object-store URL** — and the console opens it with
`window.open(url, '_blank', 'noopener')`.

Three consequences follow from this being a presigned URL:

- **It is short-lived.** The link expires. Its lifetime is set by the
  deployment's object-store presigning configuration; neither the
  procedure's response nor the screen surfaces an expiry value, so do not
  assume one.
- **It is minted on demand, per click.** Nothing caches it and nothing
  stores it on the index row. If a link has expired, press **Play**
  again to mint a fresh one.
- **It carries its own authorization.** Anyone holding the URL can fetch
  the object until it expires, with no further login. Do not paste
  playback links into tickets, chat, or bug reports — share the
  `storage_key` instead and let the recipient mint their own link.

While the mutation is in flight, every **Play** button on the screen is
disabled (`playback.isPending`), so only one link is minted at a time.

## Retention and deletion

The index row and the stored object are deleted independently, and this
screen does not delete either one. `list` is a read, and `playbackUrl`
only mints a link — no procedure reachable from the Recordings screen
removes a recording, and the table has no delete control.

Because the two artifacts are separate, they can drift: an index row can
outlive its object. When that happens the row still lists normally, and
the failure only shows up when you try to play it.

## Listing and fetching over the API

Both procedures live under `adminQuickdesk.recordings` on the
control-plane API and authenticate the same way as the rest of the
control plane — see [Authentication](/concepts/authentication).

List a page of recordings. The console requests `limit: 100, offset: 0`;
page through by advancing `offset`:

```ts
const { recordings, total } = await adminQuickdesk.recordings.list.query({
  limit: 100,
  offset: 0,
});

for (const r of recordings) {
  console.log(r.guid, r.file, r.peer_id, Number(r.size), r.created_at);
}
```

Then mint a playback link for a specific row and download the bytes
before the link expires:

```ts
const { url } = await adminQuickdesk.recordings.playbackUrl.mutate({
  storageKey: recordings[0].storage_key,
});

// `url` points straight at the object store. Fetch it without
// ClutchCall credentials — the presigned URL is the credential.
const mp4 = await fetch(url);
```

Mint the link immediately before you use it. Minting links ahead of time
for a whole page of results means most of them will have expired by the
time you get to them.

## When a playback link fails

If `playbackUrl` rejects, the console renders the message under the
table:

```
Could not open recording: <message>
```

The row stays in the list — the index row is untouched by a failed mint.
Retry the same row to find out whether the failure was transient. If a
row consistently fails to produce a usable link, check whether the
object named by its `storage_key` is still present in the bucket; the
index row and the object are stored separately and one can be missing
while the other remains.

## Related

- [Authentication](/concepts/authentication) — API keys for control-plane procedures
- [Telemetry](/platform/telemetry) — where call-side recording URLs surface in CDRs
