# 0018 · Meetings are held through a meetings service and recorded one person to a track

**Status:** Accepted · 2026-10-19. Builds on [0017](https://codna.me/docs/adr/0017-recordings-become-raw-transcripts.md) (recordings
become raw transcripts through a transcription service) and [0010](https://codna.me/docs/adr/0010-mcp-first-no-llm-inside.md).

## Context

[0017](https://codna.me/docs/adr/0017-recordings-become-raw-transcripts.md) let people bring a meeting into raw by recording it:
uploading a file from another tool, or recording the room in the browser. Both hand the transcription
service one track with everyone on it, and the service can only guess who is who: the transcript says
"Speaker 1" and "Speaker 2", and two similar voices merge. Naming them is left to whoever reads it.

Most meetings now happen online, and an online call knows who is speaking: each person's audio arrives
on its own stream, under the name they joined with. The user decided that people start meetings from
their workspace and hold them as video calls, that guests join by link, that each person's audio is
recorded on its own track, and that the meeting is filed in the starter's raw space with a transcript
that names real people.

A call needs a media server, TURN, recording egress and storage for the recordings while the call is
put together: a great deal of moving infrastructure, and none of it the kind of thing the core is.

## Decision

**The core holds no calls. It speaks to a meetings service** named by `CODNA_MEET_URL`, through a small
versioned contract ([meetings.md](https://codna.me/docs/meetings.md)), as it speaks to a transcription service: start a
meeting with an idempotency key, get a one-time link into the call for someone, ask how it stands, fetch
its files, end it, let go of it. The hosted service runs on LiveKit beside the console; a self-hoster can
put their own media server behind the same contract, or set nothing and have no meetings.

**A meeting is a recording whose page comes first.** Its raw page is filed the moment it starts, saying
it is in progress, so the meeting has an address from the beginning: the place people join from, and
where the recording arrives. The worker that takes recordings one step at a time follows a meeting too,
asking the service every half minute while the call is on. Once the service has put the recording
together, the worker keeps the mix as the page's original, keeps each person's track beside it, and
lets go of the meeting at the service, which deletes its copy. From there the meeting is a recording like
any other ([0017](https://codna.me/docs/adr/0017-recordings-become-raw-transcripts.md)): raw gains versions only from its source,
and the transcript is the source's own text arriving late.

**The tracks go to the transcription service together**, each under its speaker's name and with its
offset on the meeting's clock, as an optional addition to transcription contract v1: a service that
says `"tracks": true` in its policy takes a multipart job of up to 50 tracks and answers with the names
it was given as speakers. A service that does not, or a meeting with more tracks than that, gets the mix
alone. The tracks are kept until the transcript arrives, so asking for it again still names people, and
then dropped: the mix is the record.

**The page is the meeting's home, so it decides who joins.** Whoever can read the page can join the
call, as sharing a recording's page shares the recording. Inviting someone from the workspace shares
them the page and tells them, in their inbox and in a banner with Join in whatever CoDNA page they have
open while the meeting is on. Guests join by the link the page shows. Whoever started the meeting can
rename it and end it for everyone. The service knows nothing of CoDNA's permissions: it is told whom to
let in, one link at a time, and links back to the page for inviting more.

**People are told before they are recorded, once.** While the transcription service keeps recordings,
its notice is sent to the meetings service, which shows it on the camera check everyone joining passes,
guests included, beside its own "this meeting is recorded". Saying it again on the page would make it
one more line people learn to skip.

## Consequences

- The core gains no media code and no media keys: no WebRTC, no LiveKit SDK, no egress, no buckets. It
  gains one client, one worker step, and two small tables (`meetings`, `recording_tracks`).
- A meeting's page exists before its original does. Until the call is over the page holds no recording
  and cannot be transcribed; a meeting that recorded nothing, or whose service failed or lost it, gets a
  version saying so and stays without one.
- What a call costs in storage passes through the core once: the mix stays as every original does, the
  tracks only until the transcript is written. The service keeps nothing once let go of.
- Speaker names come from the names people joined under. A guest who types a nickname is transcribed
  under it; two people with the same name are told apart by the service.
- The contract is a boundary like transcription's: changing it is a `/v2`, adding an optional field is
  not, and the core and the hosted service agree a change before either ships it.

## Alternatives rejected

**Calls in the core,** with a media server embedded or linked. It breaks the single binary and puts TURN,
media keys and recording storage into every self-hoster's configuration, for a feature many will not
want.

**Linking out to an existing meeting product** (Meet, Teams, Zoom) and fetching its recording. Each
gives one mixed recording at best, with speaker names only by its own diarization and only on some
plans, needs an integration and a consent screen per vendor, and keeps the meeting under someone else's
retention. It remains possible as an import, as uploading their recordings is now.

**Sending the mix and naming speakers afterwards,** by matching voices to the participant list. It is
the guess the tracks exist to avoid, and it fails on exactly the meetings that matter: similar voices,
people talking over each other, a guest nobody enrolled.

**Filing the page when the recording arrives.** The meeting would have no page to join from, nowhere to
show the guest link, and nowhere to say what went wrong if the recording never came.
