Show who is on a channel
Track connected subscribers and update presence data on a channel.
Enable presence to show who is connected to a channel. Browsers join the roster when they subscribe, and your backend can read each member or just the count. Presence is disabled by default on each namespace.
Before you start
- A registered namespace and Owner or Admin access to change its policy.
- A backend token endpoint and connected browser client, as in the Realtime quickstart.
- A token with
presencein the channel’sops. Enable the namespace setting before issuing it, or policy will remove that operation.
This example uses a chat application, chat:room-1, and a member named Ada. Replace the channel, user and display metadata with your own.
Turn presence on
- In Realtime → Namespaces, open the namespace and enable Presence. The setting applies to all its channels, and the list shows a Presence chip. For MCP, use
realtime_namespace_updatewithpresence: true. - Issue fresh tokens with
presencein theopsof each channel the browser should join. See authentication.
Reading or joining a roster returns
403if presence is disabled on the namespace.
You can inspect a roster in Realtime → Presence by entering a channel name. It shows identities, connection counts, metadata and last-seen times. The Test presence panel lets you enter a member ID, TTL in seconds and JSON metadata to test membership without a browser client.
Enter presence from the browser
- Subscribe with presence options. Use the callbacks to render the roster:
const sub = socket.subscribe('chat:room-1', onMessage, {
presence: {
info: { name: 'Ada', color: '#4f46e5' },
onState: (members) => setRoster(members),
onJoin: (member) => upsert(member),
onLeave: (identity) => remove(identity),
},
})info holds your application’s display metadata, such as a name or avatar. Its serialized size must not exceed 10 KB.
After reconnecting, the client resubscribes and re-enters presence automatically.
Update and leave
- To change this connection’s metadata, replace its
info:
await socket.updatePresence('chat:room-1', { name: 'Ada', typing: true })The update resolves when acknowledged. It rejects if the connection is not open, has not joined this channel’s presence, or sends metadata above the size limit.
A member leaves after its last connection closes or remains disconnected long enough. The browser has no explicit leave method. Unsubscribing removes local subscription state but does not remove the roster entry.
Leave events are delayed so a brief reconnect does not appear as a departure. The member leaves only after the delay and when no connections remain.
The snapshot and the deltas
Render a full snapshot first, then apply membership changes from these callbacks:
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 has an identity, a connections count and its info. Delegated users appear with the ext: prefix: a token issued for user-42 produces ext:user-42. Use the returned identity as your roster key.
Build your roster with these rules:
- Replace it on every
onStatesnapshot. This repairs changes missed during a disconnection. - Insert or replace on
onJoin. It can report another tab or changed metadata for an identity already present. - Treat changes as best effort. A missing
onLeavedoes not prove the user is still connected; the next snapshot corrects the roster. - Count identities separately from connections. Three tabs for one person produce one member with
connections: 3.
You can also render the roster already held by the client:
const members = socket.presence('chat:room-1')Before the first snapshot, this returns an empty list.
Verify it worked
- Join
chat:room-1with one tab in an otherwise empty test channel. ConfirmonStateincludes that member. - Open another tab as the same user. Confirm one member with
connections: 2. Two members indicate different identities. - Read with
presence.geton your backend. Confirm the sameext:identity. - Close both tabs and wait for the leave delay. Read again and confirm the member has left.
If the presence read returns 403, check whether presence is enabled. An empty roster is a successful read with no members.
Read the roster from your backend
Use your backend’s product public key to call the presence routes. With the client from @lepsto/sdk/realtime/server:
const { members } = await realtime.presence.get('chat:room-1')
const { members: count } = await realtime.presence.stats('chat:room-1')The MCP equivalents are realtime_presence_get and realtime_presence_stats.
The roster response returns one entry per identity:
{
"channel": "chat:room-1",
"members": [
{ "identity": "ext:user-42", "connections": 2, "info": { "name": "Ada" }, "ts": 1754130000123 }
]
}ts records the last member update in milliseconds. The stats response has a numeric members field counting distinct identities, regardless of tab count. Use it when your interface needs only a count.
Product public keys authorize roster reads and counts, not presence writes. Connected browsers enter and update their own membership over the socket.
Enter presence for something with no tab
For a bot, job, service account or disconnected user, use presence.enter, presence.update and presence.leave from @lepsto/sdk/realtime/server. These administrative writes require a bearer token and explicit base URL; a product public key cannot authorize them. Users with an active socket should join through that socket. See the server SDK.
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.