The recordings screen lists recorded audio for the active organisation. It does not read from a single recordings table. It merges two sources — call detail records that carry a recording_url, and objects listed from the recording bucket — then applies one shared filter predicate to the merged set. Understanding that merge explains most of the screen’s surprising behaviour.

Where the list comes from: CDRs plus bucket objects

cdr.recordings runs two lookups in parallel for the active org: Because a single call can produce several CDR rows, the CDR side groups by call_id first. Within a group the screen shows:
  • max(starting_time) as the timestamp,
  • max(duration) and max(recording_size_bytes),
  • the latest-by-starting_time value for caller, callee, direction, agent and recording_url.
The two sides are then merged into a map keyed by call id. Bucket objects are applied after the CDR rows, so when a call appears in both, the bucket object wins: its recording_url becomes s3://<bucket>/<object-key> and its sizeBytes becomes the displayed file size. The call metadata (participants, direction, duration, agent) still comes from the CDR lookup. For every bucket object, the procedure also queries cdrs for that tenant restricted to the listed call ids, to recover participant metadata that the object itself does not carry.

Rows without CDR attribution, and shared-bucket safety

An object in the bucket knows its call id, its size and when it was last modified. It does not know who was on the call. That metadata only exists in cdrs. So two outcomes are possible when no matching CDR row exists for the tenant: The object came from the org’s own storage. The row is still shown. Caller and callee fall back to empty strings and render as —, direction and duration are null, and the timestamp falls back to the object’s last-modified time rather than a call start time. This is what a recording with blank participants means: audio exists in storage, but no CDR for this tenant references that call id yet — for example because the CDR has not been written, or because it was aged out of the cdrs table while the object survived in the bucket. The object came from a shared bucket. Objects whose source is shared and which have no cdrs row for this tenant are dropped entirely. There is no proof the call belongs to the active org, and the procedure will not surface an unattributed object across tenants. The CDR row is the tenant proof. If a shared-bucket recording is missing from the list, the missing piece is the CDR attribution, not the object.

Signed playback and download URLs

The grid never links directly to a storage object. When you press play or download, the console resolves recording_url first:
  • A URL that does not start with s3:// — a bring-your-own destination — is passed through unchanged.
  • An s3:// URI is exchanged for a presigned GET by calling storage.signedUrl with the org id and the call id.
Presigned URLs from that call are valid for one hour. The console requests one per click and does not persist it, because the URL expires. A URL that you copy out of the network tab or out of the audio element will stop working once that hour is up; re-open the screen and click again to mint a fresh one. Play hands the resolved URL to the shared player with the call id as the track id, so pressing play on the row that is already loaded toggles pause instead of re-resolving. Download resolves the same way and saves the file as <call_id>.wav.

Filtering, paging and how the summary cards are computed

Every filter is applied server-side, over the merged set, before paging: Rows that survive the predicate are sorted newest first by starting_time, then sliced with offset and limit. limit defaults to 200 and the input schema accepts 1–500. The four cards at the top are computed by the server over the filtered set as a whole, not over the returned page:
  • Total Recordings — the count of filtered rows, the same number as total in the footer’s “Showing N of M recordings in range”.
  • Total Duration — the sum of every row’s duration, treating null as zero.
  • Storage Used — the sum of every row’s recording_size_bytes.
  • Avg Duration — total duration divided by the number of rows with a duration greater than zero, rounded. Rows with no duration, including unattributed bucket objects, are excluded from the divisor but still counted in Total Recordings.
That divisor is why Total Duration ÷ Total Recordings will not always equal Avg Duration. The query refetches every 60 seconds while the screen is open.

The features.recording entitlement gate

The screen reads the org’s entitlement claims and checks claims.features.recording. The list renders unless the claims are loaded and the flag is explicitly false, in which case the screen is replaced by an upsell panel and no recordings query result is shown. The gate deliberately fails open in two cases:
  • entitlement data has not loaded yet, and
  • entitlement data is null, meaning the entitlement service is not configured for this deployment.
So an unconfigured entitlement service never hides recordings from a tenant that has them.

Deleting a recording

Each row carries a delete control in the actions column. In the screen as shipped, that control has no handler attached: clicking it does not call a procedure, does not remove the object from the bucket, and does not change the CDR. Nothing is deleted, and the row returns on the next refetch. Treat the recordings grid as read-only until a delete procedure is wired to it.