# Room lifecycle and names

> How a meeting room comes into existence on first join, when the engine reaps it, what a room name may contain, and what closing a room does to joins that arrive after it.

A meeting in ClutchCall is not a stored object. It is a **room name plus a
token**. The conferencing engine (`mod_meet`) has no create API: it
materialises a room the first time a participant joins with a token that
carries that name, and it reaps the room when the last participant leaves.

Everything the console shows under **Rooms** is therefore live engine state,
read back from the engine on each poll — not a database table you can pre-seed,
rename, or archive.

| Thing | Where it lives |
| ----- | -------------- |
| Room name | In the token you mint, and in the join URL |
| Room | In the engine, for as long as somebody is in it |
| Participants, tracks, permissions | In the engine, per room |
| Per-org engine address + signing key | Control-plane config, keyed by org |

## When a room is created and when it is reaped

1. You pick a name and mint a token for it.
2. A client joins with that token. The engine creates the room at that moment.
   This is the only way a room comes into existence — there is no
   `rooms.create` procedure and no "create room" action in the console.
3. The room is listed by `meet.rooms.list` with a participant count and a
   `createdAt` timestamp (the console renders `createdAt` as the room's
   "up" time).
4. When the last participant leaves, the engine reaps the room. Its name
   disappears from the listing, along with its participant and track state.

Because creation is join-driven, a name you have only minted a token for does
not appear anywhere until media actually starts flowing. If you just clicked
**New meeting** and the room is not in the list yet, nobody has joined it yet.

Reaping is the engine's decision, not the console's. The console does not
display, and this page does not define, a fixed delay between the last leave
and the reap — treat an empty-but-listed room as a normal transient state
rather than an error. See
[Empty rooms: why one can still be listed](#empty-rooms-why-one-can-still-be-listed).

## Room name grammar

A room name is carried in a token claim, validated by the control plane's
`roomName` schema, and then used as an element of the MoQT namespace for the
room's tracks. That last point is the constraint that matters: the name has to
survive being placed in a namespace path, so it is restricted to **unreserved
characters only** — no percent-encoding, no slashes, no spaces, nothing that
would need escaping to sit in a namespace element.

Practical consequences:

- A name is an opaque identifier, not a title. Put the human-readable subject
  of the meeting in your own application data, not in the room name.
- Two different names are two different rooms, even if they differ only in
  case or punctuation. Nothing normalises them for you.
- A name that the schema rejects fails at the control plane before the engine
  is ever contacted, so the failure surfaces as a rejected mutation rather
  than as a transport-level join error.

### Names the console mints

**New meeting** does not ask you for a name; it generates one and navigates
straight into the join flow. The generated shape is:

```
m-xxxx-xxxx
```

The `x` positions are drawn with `crypto.getRandomValues` from a 32-character
alphabet — `abcdefghijkmnpqrstuvwxyz23456789` — which is the lowercase
alphanumerics with the visually ambiguous characters (`l`, `o`, `0`, `1`)
removed. The names are random rather than sequential, so holding one room's
name tells you nothing about any other room's name.

You are not required to use this shape. It is simply a name that is guaranteed
to satisfy the schema, safe to read aloud, and not guessable from a sibling.

## How the name becomes a namespace element

The room name is the element that separates one room's media from another's in
the MoQT namespace. Tokens are scoped to namespaces, so the room name in the
token is what gates publish and subscribe for that room: a token minted for one
room name cannot carry media for a different one.

Two things follow from this:

- **Treat the name as a capability-adjacent value.** Anyone who holds a valid
  token for a name can join that room. Do not derive names from sequential
  identifiers, and do not reuse a name across unrelated meetings if the
  tokens for the earlier meeting have not expired.
- **A name change is a new room.** There is no rename. Moving a meeting to a
  different name means minting new tokens and having clients join the new
  name; the old room reaps itself once its last participant leaves.

## Empty rooms: why one can still be listed

Opening **Participants** on a room can show "Nobody is in this room". That is
not a bug and not an inconsistent read. It means the engine still holds the
room, but no participant is currently in it — the room has not been reaped yet.

You can also see a room that looks emptier than its own header suggests:

- The room header reports `numParticipants` and, when it differs,
  the number of participants that are **hidden** (participants whose
  `hidden` permission is set). Hidden participants are counted in the room,
  and the difference between the total and the visible count is shown
  explicitly in the room subtitle.
- A participant that has just joined can appear with no tracks at all. The
  engine registers a track when it first carries media, so an empty track
  list means nothing has arrived from that participant **yet** — it is not a
  statement that they are refusing to publish.

The listing is polled, not pushed: the room list refetches every 15 s and an
open participant table refetches every 10 s. A room that was reaped a moment
ago can therefore still be on screen until the next poll. **Refresh** forces
a re-read.

## Closing a room versus waiting for it to reap

The **Close** action on a room card calls `meet.rooms.remove`. It does not
delete a stored record, because there is no stored record. It **ejects every
participant currently in the room**, which is what causes the engine to drop
the room.

| | Waiting for the reap | Closing the room |
| --- | --- | --- |
| Trigger | The last participant leaves on their own | An operator calls `meet.rooms.remove` |
| Effect on participants | None; they have already gone | Everyone is disconnected immediately |
| Effect on the name | Freed when the room is reaped | Freed as the room drops |
| Effect on issued tokens | None | None — tokens stay valid |

Removing a single participant (`meet.participants.remove`) is the narrower
version of the same idea: that person is disconnected immediately, and they can
rejoin with a valid token. Neither action revokes anything.

If a close is rejected, the console surfaces the error on the room card rather
than optimistically dropping the row — a room that is still listed after a
failed close is still open.

## Rejoining a name after a close

Because a close only ejects participants, and because tokens are unaffected by
it, a join that arrives after the close **re-creates the room under the same
name**. This includes joins that were already in flight when you clicked
**Close**: they are racing the eject, and whichever lands last wins. A client
that reconnects automatically will therefore reappear in a room you just
closed.

Closing a room is consequently a way to clear a room, not a way to end a
meeting permanently. To make a name unjoinable you have to stop honouring it
at token-mint time — stop minting tokens for that name, and let the tokens
already out there expire. See
[Authentication](/concepts/authentication) for how token expiry and signing-key
rotation interact.

The reverse case is the ordinary one: a room that reaped itself is recreated by
the next join with a valid token for that name, with fresh participant, track,
and permission state. Nothing carries over from the previous occupancy.

## Joining requires a reachable engine

Rooms are read from the engine, so the console checks `meet.status` before it
lets you into a room. The check distinguishes three states, and they mean
different things for room lifecycle:

| `meet.status` | Meaning | Rooms listing |
| --- | --- | --- |
| `configured: false` | No engine and signing key are configured for this org | No rooms can be read or created |
| `configured: true`, `reachable: false` | Config exists, but the control plane did not answer | No rooms can be read; `detail` carries the error |
| `configured: true`, `reachable: true` | Engine answered; `detail` names the shard | Rooms are listed live |

**New meeting** is disabled unless the engine is reachable. This is deliberate:
joining a room on an unreachable engine fails at the transport with a much less
legible error than a disabled button.

Configuration is per organization — each org has its own engine address and its
own rotatable signing key — so a room name is only ever meaningful within the
org whose engine holds it.

## Related

- [Authentication](/concepts/authentication) — minting the token that carries
  the room name, and namespace scoping
- [Telemetry](/platform/telemetry) — the operational streams the gateway emits
