Skip to content

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

  1. Set presence: true on the namespace. It applies to every channel under that namespace. Namespaces are configured from the platform — over MCP, realtime_namespace_update.
  2. Mint tokens with the presence operation. A browser needs presence in the channel’s ops for 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:

CallbackWhen 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 onState you 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.
  • onJoin is an upsert, not strictly a join. The same identity can arrive more than once — a second tab, a changed info. Key your roster by identity and 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 onLeave as 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

Was this page helpful?
Esc

Start typing to search the docs.

navigateselect