Realtime
Show who is on a channel
Turn presence on for a namespace, join the roster from the browser, and read it from your backend.
Presence keeps a roster of who is currently on a channel. Your users’ browsers join and leave it; your backend reads it. It is off by default and turned on per namespace.
Turn presence on
- Set
presence: trueon the namespace. It applies to every channel under that namespace. Namespaces are configured from the platform — over MCP,realtime_namespace_update. - Mint tokens with the
presenceoperation. A browser needspresencein the channel’sopsfor every channel whose roster it will take part in. See authentication.
Reading or joining presence on a namespace that does not have it enabled is refused with
403.
Enter presence from the browser
A browser enters presence as part of subscribing to the channel:
const sub = client.subscribe('chat:room-1', onMessage, {
presence: {
info: { name: 'Ada', color: '#4f46e5' },
onState: (members) => setRoster(members),
onJoin: (member) => upsert(member),
onLeave: (identity) => remove(identity),
},
})info is opaque metadata you attach to the member — a display name, an avatar, whatever your interface needs. It must serialize to no more than 10 KB.
The client resubscribes on its own after a reconnect, so the member re-enters the roster without you doing anything.
Update and leave
Replace this connection’s info at any time:
await client.updatePresence('chat:room-1', { name: 'Ada', typing: true })The promise resolves when the service acknowledges the update. It rejects if the connection is not open, if this connection is not a presence member of the channel, or if the new info is too large.
A member leaves when its last connection to the channel goes away — the tab closes, the client is closed, or the network drops for long enough. There is no explicit leave call in the browser client: dropping the subscription clears local state but does not remove the member from the roster.
Because a short reconnect should not look like a departure, a dropped connection only produces a leave once the delay has passed and the member has no connections left. A client that comes straight back stays in the roster.
The snapshot and the deltas
Your client sees the roster through one snapshot and two deltas:
| Callback | When it fires |
|---|---|
onState(members) | On the initial subscribe and again after every resubscribe, with the full roster |
onJoin(member) | A member entered or its info changed |
onLeave(identity) | A member left |
Each member carries identity, connections — how many live sockets that identity has on the channel — and the info it entered with.
The guarantees to build on:
- The snapshot is authoritative. Rebuild your local roster from every
onStateyou receive rather than trusting deltas accumulated since the last one. That is what makes a reconnect self-healing: the fresh snapshot replaces anything you missed while disconnected. onJoinis an upsert, not strictly a join. The same identity can arrive more than once — a second tab, a changedinfo. Key your roster byidentityand replace the entry.- Deltas are best effort. They can be missed across a connection drop; the next snapshot is what puts you right. Do not treat a missing
onLeaveas proof that somebody is still there. - The roster is deduplicated by identity. One person with three tabs is one member with
connections: 3, not three members.
The client also exposes the roster it currently holds, which is convenient for rendering:
const members = client.presence('chat:room-1')It is empty until the first snapshot arrives.
Read the roster from your backend
Your backend reads the roster and the count with your product API key, on the two presence routes listed in what your API key reaches. With the server SDK:
const { members } = await realtime.presence.get('chat:room-1')
const { members: count } = await realtime.presence.stats('chat:room-1')Over MCP, realtime_presence_get and realtime_presence_stats.
The roster response returns one entry per identity:
{
"channel": "chat:room-1",
"members": [
{ "identity": "user-42", "connections": 2, "info": { "name": "Ada" }, "ts": 1754130000123 }
]
}ts is when that member was last updated, in milliseconds. The stats response returns members as a plain number — the count of distinct identities present, not the number of connections. Use it when you only need “12 people here” and do not want to transfer the roster.
Presence writes are not part of the public route set. A backend holding a product API key reads the roster and the counts; the browser client is what enters, updates and leaves presence.
Next steps
- Receive channel events on your backend: be told about members entering and leaving instead of polling the roster.
- Connect a browser tab: the presence options in full, alongside subscribe and reconnect.
- Authenticate your backend and your users: mint a token carrying the
presenceoperation. - Look up a limit or an error: roster size,
infosize and what403means here.