Conferencing in TeleQuick is multi-party audio and video carried over MoQT. The console exposes four screens over it: an Overview that answers “is this set up and does the engine answer”, a Rooms moderation surface, a Join screen that opens a real media session, and Engine & keys for the per-organization engine URL and signing secret. Everything the console shows about rooms and participants is read live from the conferencing engine (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.
Room names come from the 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 calls meet.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 — sets canPublish: false on the minted token, for an observer who should receive media but never send it.
Both are decided at mint time, on the server. A participant already in a room can still have their publish permission changed afterwards from the Rooms screen.

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:
It returns one entry per remote video track, keyed <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, decoderStarted false — media is arriving but no decoder ran. Look at keyframes: without receivedKeyframes there is nothing to start from.
  • decoderStarted true, attachedElements zero — 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.
Until either fires you get a progress line and a Cancel button back to Rooms. Cancelling, or navigating away, runs the screen’s cleanup, which disconnects the half-open session — without that, the camera light stays on and you stay in the room for everyone else. On the Rooms screen, New meeting is disabled while the engine is not reachable. Joining an unreachable engine fails at the transport with a far less legible message than a disabled button.

What’s in the console

Read the overview and check the engine

Overview polls meet.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 rooms from the health probe — every room the answering shard still holds, including one a join opened and never finished tearing down.
When they differ, the tile labels the engine number as a shard count that includes rooms still tearing down. When they match, it reads as rooms held on the answering shard. Neither number is stale data from a job; both are live reads of different things.

Visible vs hidden participants

Each room row reports numParticipants 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 queries meet.participants.list and polls it while open. Each participant row gives you:
  • Tracks — one button per track. Clicking it calls meet.participants.mute with the track sid and the inverted muted state.
  • Permissions — five toggles, each calling meet.participants.update.
  • Remove — meet.participants.remove, after a confirmation.
The room header additionally offers Close (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 a sid, 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 with meet.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 the iss 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.
So rotation blocks pending joins and leaves live calls alone. That is the tool for a leaked or suspect secret.

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.
  • Authentication — API keys, relay tokens, and namespace scope
  • Telemetry — metrics and traces emitted by the gateway