Skip to content

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 presence in the channel’s ops. 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

  1. 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_update with presence: true.
  2. Issue fresh tokens with presence in the ops of each channel the browser should join. See authentication.

Reading or joining a roster returns 403 if 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

  1. 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

  1. 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 onState snapshot. 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 onLeave does 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

  1. Join chat:room-1 with one tab in an otherwise empty test channel. Confirm onState includes that member.
  2. Open another tab as the same user. Confirm one member with connections: 2. Two members indicate different identities.
  3. Read with presence.get on your backend. Confirm the same ext: identity.
  4. 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

Was this page helpful?
Esc

Start typing to search the docs.

↑↓navigate↵select