mod_meet). There is no sampling and no rollup, so an
empty room list means the engine reported no rooms.
What a meeting is: a name plus a token
mod_meet has no create-room API. A room is created on first join and
reaped when the last participant leaves. A meeting is therefore a room
name plus a token that authorises you to join that name.
Consequences that surprise people:
- There is no “create room” action in the console. New meeting on the Rooms screen mints a name and navigates you into it; the room appears in the list once someone is actually in it.
- A room is live engine state, not a stored record. Nothing persists a room between the last leave and the next join.
- Close on a room does not delete a record — it disconnects everyone currently in it. The same name reappears in the list if somebody joins again.
roomName schema the control-plane API enforces, and
the name becomes an element of the MoQT namespace. The console’s generator uses
unreserved characters only, and draws them randomly rather than sequentially so
one room name does not reveal another.
Joining: token minting and per-join identity
The Join screen callsmeet.join with the org and the room name. The
control-plane API mints the token and returns it together with the join URL.
The client never supplies its own identity — if it could, anyone could join as
anyone.
meet.join accepts:
It returns
token, expiresAt, url, the derived identity, and
existingSessions.
Identity is derived per join, not per user. The server builds it as the
authenticated user id plus a short random nonce, joined with ~
(<user-id>~ab12cd). The prefix keeps a session attributable to an account;
the nonce makes each join its own participant.
This matters because the bare user id was not safe. With it, one account joined
from a laptop and a phone claimed the same MoQT namespace and published
competing tracks under the same names; the relay’s namespace home flipped
between shards and viewers saw the feed alternate between two fighting
publishers. Per-join identity gives each device its own participant, the model
you already expect from consumer conferencing.
The join token is minted with admin: true when the caller is an org admin
(admin grant, or the owner/admin org role). That is what makes the in-call
moderation controls work without a second round trip. Everyone else joins as a
plain participant.
Multiple devices on one account and “switch here”
Because identity is per join, the same account can legitimately be in a room more than once. Before minting,meet.join lists the room’s participants and
returns the ones whose identity carries the caller’s own <user-id>~ prefix as
existingSessions. That is the signal behind a “you’re already in this
meeting — switch here?” prompt.
The lookup is best-effort: if the engine does not answer the list call, the
join still proceeds with an empty existingSessions.
A companion procedure lets a caller kick their own other sessions out of a
room. It is viewer-gated rather than admin-gated because it is hard-scoped
server-side to identities carrying the caller’s own <user-id>~ prefix, so it
can never remove anybody else.
Publish scope: sources and subscribe-only joins
Two token fields shape what a join can send:sources— the set of sources the token permits publishing. Omit it to take the engine’s default for the token.subscribeOnly— setscanPublish: falseon the minted token, for an observer who should receive media but never send it.
Diagnosing no video: arrived, decoded, attached
When a participant reports “I can hear them but I can’t see them”, the page itself tells you nothing: an empty tile looks identical whether nothing was delivered, frames arrived and were never decoded, or they decoded and were never attached to an element. Working audio already proves the session, the namespace and the transport, so guessing between those three is what wastes the time. The Join screen installs a diagnostics handle for exactly this. From the browser console, in a joined session:<identity>/<source>:
How to read it:
- All counters at zero — nothing is arriving. The problem is upstream of this participant: the publisher, the subscription, or namespace routing.
- Counters climbing,
decoderStartedfalse — media is arriving but no decoder ran. Look at keyframes: withoutreceivedKeyframesthere is nothing to start from. decoderStartedtrue,attachedElementszero — the track decoded and was never attached to an element. The failure is in the UI layer, not the transport.
window.__meet.room is the same Room object the UI is driving, if you need
to inspect it directly.
When the engine does not answer
The Join screen has two timers, both cleared the moment the room connects or the join fails on its own:- At 12 seconds it marks the join as slow — “Still connecting to
<room>…” plus a pointer at Engine & keys. - At 30 seconds it gives up with a real error: the conferencing engine did not answer, check the engine URL.
What’s in the console
Read the overview and check the engine
Overview pollsmeet.status and, only once the engine is known to answer,
meet.rooms.list. Gating the second query matters: polling rooms against an
unreachable engine costs a control-plane timeout and reports the same failure
twice.
meet.status returns four fields and three meaningful states:
“Not set up” is a first-class state, not an error — an org that has just been
granted the modality lands there, and the only useful next step is Engine &
keys.
The Engine tile’s subtitle shows which shard answered. That number changes
between reloads because health probes are load-balanced across the engine’s
reactor shards; a different shard answering is normal, not a fault.
Registry count vs engine shard count
Overview shows two counts and they can legitimately disagree:- Active rooms is the length of the room list from the engine registry.
- Reported by engine is
roomsfrom the health probe — every room the answering shard still holds, including one a join opened and never finished tearing down.
Visible vs hidden participants
Each room row reportsnumParticipants and numVisible. A hidden participant
— a recorder, an observer — is in the room but is not rendered to peers. The
console shows both numbers and tags the difference (+2 hidden) so the gap
reads as a deliberate state rather than a mismatch.
hidden is one of the five permissions you can toggle per participant.
Moderate a live room
Expanding a room on the Rooms screen queriesmeet.participants.list and
polls it while open. Each participant row gives you:
- Tracks — one button per track. Clicking it calls
meet.participants.mutewith the track sid and the inverted muted state. - Permissions — five toggles, each calling
meet.participants.update. - Remove —
meet.participants.remove, after a confirmation.
meet.rooms.remove) and a copy
button for the room name.
A rejected mutation is surfaced on the row it came from rather than silently
leaving a toggle looking like it worked.
The participant permission model
Five flags, gating five different things:meet.participants.update takes the whole permission object, not a patch —
its schema requires every field, and sending a partial would silently clear the
ones you omitted. The console always spreads the participant’s current
permissions and overrides the single flag being toggled.
Tracks, sources and mute
Each track in the list carries asid, a kind (audio, video, or data)
and a source. Mute is per track: you pass the trackSid and the desired
muted state, so muting a camera does not touch a microphone.
An empty track list is a real state, not an error. The engine registers a
track when it first carries media, so a participant who just joined, or who has
both devices off, has no track data yet. The console says “No track data”
rather than “not publishing” — absence of a frame is not evidence about
somebody’s intent.
Likewise, a room can be listed with no participants: it is still open
because the engine has not reaped it yet.
Removing a participant vs closing a room
Neither is a permanent block. Both are confirmed before they run, because both
are immediately visible to everyone in the call. Revoking access durably is a
matter of the token — see rotation below.
Point an organization at a conferencing engine
Engine & keys holds the per-org conferencing config, read withmeet.config.get and written with meet.config.set. This replaced what used
to be process environment, so a single deployment can serve many
organizations, each with its own engine and its own rotatable secret.
Three things live on this screen, with very different blast radii:
The Engine URL field accepts
http(s):// or ws(s):// and normalises it —
the same host serves the media plane over QUIC and the control plane.
Saving an engine URL for an org that has none generates an issuer and a signing
secret and pushes them to the engine. The confirmation banner is driven by
hasSecret on the mutation’s own response, so it cannot claim conferencing is
enabled while the rest of the screen still reads “not configured”.
The issuer and the signing secret
The issuer is theiss claim the engine uses to select which secret to verify
a join token against. It is unique per organization, and it is safe to read and
copy — the console shows it in a read-only field with a copy button.
The signing secret is different. The control-plane API projects it away before
the response leaves the server; a browser only ever receives hasSecret, and
the console renders that as set · not displayed or missing. If hasSecret
is false, the engine URL is saved but conferencing is not finished being
enabled — no join can be authorised until a signing key exists.
Rotate the secret without dropping participants
Rotate (meet.config.rotateSecret) keeps the issuer and replaces the
secret. Rotation is a separate, deliberate action rather than a field on the
form, because of what it does:
- Every join token minted under the old secret stops verifying once the engine re-reads its keys. Anyone mid-join has to request a new token.
- Participants already connected are not disconnected.
Re-push keys when the engine has drifted
Re-push keys (meet.config.resync) sends this org’s signing keys to the
engine again from the platform database. Use it when the config in the console
looks correct but the engine behaves as though it does not have the key — the
platform is the source of truth and this restates it, without minting anything
new.
Reach for re-push before rotation. Rotation invalidates every outstanding
token; re-push invalidates nothing.
Both rotation and removal take effect as soon as the engine re-reads its keys.
Remove a configuration and re-enable later
Remove (meet.config.remove) deletes the engine URL and signing key for
the organization. Afterwards no new session can be authorised. The modality
entitlement is not affected — re-enabling is a matter of setting an engine
URL again, which generates a fresh issuer and secret.
The action is confirmed and marked destructive, and Overview will fall back to
its “not set up” state for the org.
Related
- Authentication — API keys, relay tokens, and namespace scope
- Telemetry — metrics and traces emitted by the gateway