WebSocket Presence
Presence answers a simple question: who is online right now, and where? Enable it on a WebSocket endpoint and Slingshot tracks which user IDs hold open sockets in each room — the backbone of “online users” indicators, lobby lists, and “X is typing” widgets.
How it works
Section titled “How it works”Set presence: true on a WsEndpointConfig and the framework attaches per-socket bookkeeping
to the WebSocket state. When a socket subscribes to a room the user is added to that room’s
presence set; when the last socket for that user leaves, the user is removed. Multiple
sockets per user (multi-tab, mobile + desktop) collapse to a single presence entry — only the
first connection and last disconnection produce events.
Reads are O(1). getRoomPresence(state, endpoint, room) returns the user IDs currently in a
room; getUserPresence(state, userId) returns every room a user is in across endpoints.
Enable presence
Section titled “Enable presence”import { defineApp } from '@lastshotlabs/slingshot';
export default defineApp({ ws: { endpoints: { '/ws/chat': { presence: { broadcastEvents: true }, incoming: { join: { handler: (ws, payload, ctx) => { const room = (payload as { room: string }).room; ctx.subscribe(room); }, }, }, }, }, },});presence: true is the shorthand for presence: { broadcastEvents: false }. Setting
broadcastEvents: true makes the framework publish presence:join and presence:leave
events to the room when users come and go — useful when every connected client needs to react
to the change.
Reading presence in a handler
Section titled “Reading presence in a handler”// @skip-typecheckimport { definePackage, domain, getContext, getRoomPresence, route } from '@lastshotlabs/slingshot';
export const chatPackage = definePackage({ name: 'chat', domains: [ domain({ name: 'rooms', basePath: '/rooms', routes: [ route.get({ path: '/:room/presence', handler: async ({ c, respond }) => { const ctx = getContext(c.get('app')); const users = getRoomPresence(ctx.wsState, '/ws/chat', c.req.param('room')); return respond.json({ users, count: users.length }); }, }), ], }), ],});That gives you a plain HTTP endpoint clients can poll on connect to render the initial list before subscribing to live updates.
Multi-instance presence
Section titled “Multi-instance presence”Per-instance presence is local. In a multi-instance deployment one server only sees its own
sockets — instance A does not know about instance B’s connected users. Cross-instance fanout
runs through the WebSocket transport (Redis pub/sub by default), so when instance A broadcasts
a presence:join to a room, instance B’s connected clients in that room receive it.
The presence registry itself is not replicated. If you need a globally-correct online list,
fold the join/leave events into a shared store (Redis SET keyed by room) inside an
onRoomSubscribe hook.
What’s next
Section titled “What’s next”- WebSocket Recovery — session replay across reconnects
- WebSocket Transports — multi-instance broadcast
- WebSockets — full endpoint config surface