A room is the unit of presence in LevelChat. Every call, every meeting, every broadcast is a room. Rooms are cheap to create (sub-100 ms p99) and idle rooms cost nothing — they auto-archive when the last participant leaves.
Lifecycle
created → active → ended → archived- A room is
createdwhen you POST/v1/rooms. - It enters
activethe moment the first participant joins. - It enters
endedwhen the last participant leaves or when youDELETE /v1/rooms/{id}. - It enters
archived24 hours afterended(rooms then become read-only — recordings remain).
Create a room
A standalone @levelchat/node SDK is on the roadmap; until it ships, the public REST API is
all you need:
const res = await fetch(`${process.env.LEVELCHAT_API_URL}/v1/rooms`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.LEVELCHAT_API_KEY}`, // lc_pk_xxx.yyy
},
body: JSON.stringify({
external_id: 'team-standup-2026-04-25',
type: 'meeting', // meeting | live | webinar | 1to1
config: {
max_participants: 50,
preferred_codec: 'av1',
record: { enabled: false },
e2ee: false,
region_hint: 'eu-fsn',
},
}),
});
const room = await res.json(); // { id, external_id, type, config, created_at }In most apps you don't pre-create rooms — call POST /v1/auth/tokens/room directly with any
deterministic roomId your app picks (r_team-standup-2026-04-25) and the room is created on
the fly with sane defaults.
Pick the right type — canonical values are defined in
services/shared-go/capabilities/capabilities.go::RoomType:
meeting— everybody can publish. The default iftypeis omitted.live— one (or few) publishers, many viewers. Cascade SFU + LL-HLS/CMAF fallback at scale. Up to 25,000 concurrent viewers on the self-serve Broadcast Scale tier; Enterprise reserves edge capacity for larger drops. (Legacy aliasesbroadcast,one-to-manynormalize toliveat ingress for backwards compatibility.)webinar—livewith a Q&A queue + registration gating + host/panelist/viewer roles.1to1— peer-to-peer fast path for two-participant calls. Lower glue-code per room. Aliasesone-to-oneandp2pnormalize here.
Pre-promotion (hybrid) — starting as meeting and promoting to live mid-session without
dropping participants — is on the roadmap as a Preview API behind the W9.4 milestone. Until
then the supported path is: end the meeting, then have viewers joinLive() against a fresh
live room.
Token capabilities
Tokens are scoped, not roles. A publisher token with no subscribe:* cap will not see anyone.
A subscriber token with chat:send can talk in chat without ever appearing on camera.
const cap = ['publish:camera', 'publish:screen', 'subscribe:all', 'chat:send'];Common combinations:
| Use case | Capabilities |
|---|---|
| Standard meeting participant | publish:camera, publish:screen, subscribe:all, chat:send |
| Read-only viewer | subscribe:all, chat:send, reactions:send |
| Stage participant in a broadcast | publish:camera, subscribe:all |
| Classroom student | publish:camera, subscribe:all, chat:send, hand:raise |
| Moderator | moderate:participants, chat:send, subscribe:all |
Listening for events
import { LevelChat } from '@levelchat/web';
const lc = new LevelChat();
const live = await lc.joinLive({ token, role: 'broadcaster', roomType: 'meeting' });
live.room.on('participant-joined', (p) => console.log(p.identity, 'joined'));
live.room.on('participant-left', (p) => console.log(p.identity, 'left'));
live.room.on('track-subscribed', (t) => console.log(t.participantId, 'published', t.kind));
live.room.on('connection-quality', (id, q) => console.log('rtt', q.rtt, 'loss', q.fractionLost));
live.room.on('error', (err) => console.error(err.code, err.message));Ending a room
await lc.rooms.end(room.id); // or DELETE /v1/rooms/{id}This kicks every participant with reason: "host_ended" and triggers room.ended webhook.
Idle-room auto-end (no phantom usage)
A room can also end on its own. If your host's tab closes — or the last participant's connection drops — LevelChat detects the silence and ends the room within roughly one minute:
- WebSocket watchdog (≤45 s) — every participant ping/pong-checks the signaling server. A dead client (laptop sleep, network drop, browser quit without a clean close) is dropped from the room.
- Host grace period (15 s after the last host leaves) — covers page refreshes; if no host reconnects, the room is force-closed.
- Reaper safety net (≤80 s) — if a leave event was dropped by the message bus, the admin-api reaper marks the room ended on its next fast-lane sweep (≤20 s ticks) and closes any stranded participant rows.
Why it matters: usage is metered on participant minutes / viewer hours, so a stranded "still in the room" record on the server side would inflate the bill long after everybody actually left. With the auto-end you can trust that
- a scheduled room with nobody in it accrues zero usage (it
stays
state: pendinguntil someone joins); - a room that was joined and then abandoned stops accruing usage within ~60 s of the last tab closing;
- the Console + Meet dashboards reflect that timing in their real-time meters.
Where rooms appear
Every room you create — via the REST API, an SDK, or the Meet dashboard — lands in the same Postgres row scoped to the authenticated org + project. That single source of truth is what each surface reads:
| Surface | Lists rooms? | Joins by code? |
|---|---|---|
| Console → Rooms | Yes | n/a |
Meet dashboard (meet.levelchat.io) | Yes | Yes (/r/<code>) |
SDK (@levelchat/web and others) | Programmatic | Yes (joinLive/joinMeeting) |
So a room your customer's React Native app creates with lc.rooms.create()
is immediately visible in your Console workspace AND joinable from
meet.levelchat.io/r/<short_code> — the workspace owner can hop into a
customer-created room from the browser without any extra plumbing. The
opposite also holds: a room your team scheduled in the Meet dashboard
can be joined from a phone running your SDK.
Tenant isolation is enforced at the row level: an API key only sees
its own org's rooms, a logged-in dashboard user only sees their
workspace's rooms, and a /r/<code> lookup that misses the caller's
org returns 404 — never a side-channel that probes existence.