Presence
Track who’s connected to a topic in real time. Handles multi-tab dedup (same user with two tabs open = one presence entry), broadcasts join/leave events, and provides a live store on the client.
Setup
Create a shared presence instance:
// src/lib/server/presence.js
import { createPresence } from 'svelte-adapter-uws/plugins/presence';
export const presence = createPresence({
key: 'id',
select: (userData) => ({ id: userData.id, name: userData.name }),
heartbeat: 60_000 // optional: needed if clients use maxAge
}); Wire it into your WebSocket hooks:
// src/hooks.ws.js
import { presence } from '$lib/server/presence';
export function upgrade({ cookies }) {
const user = validateSession(cookies.session_id);
if (!user) return false;
return { id: user.id, name: user.name };
}
export const { subscribe, unsubscribe, close } = presence.hooks; The hooks object handles everything: subscribe calls join() for regular topics and sends the current presence list for __presence:* topics, close calls leave(). If you need custom logic (auth gating, topic filtering), wrap the hook:
export function subscribe(ws, topic, ctx) {
if (topic === 'vip' && !ws.getUserData().isVip) return false;
presence.hooks.subscribe(ws, topic, ctx);
}
export const { unsubscribe, close } = presence.hooks; Options
| Option | Default | Description |
|---|---|---|
key | 'id' | Field used for multi-tab dedup |
select | identity | (userData) → publicData - extract public fields |
heartbeat | disabled | Broadcast active keys at this interval (ms). Required if clients use maxAge |
Server API
| Method | Description |
|---|---|
presence.hooks | Ready-made { subscribe, unsubscribe, close } hooks |
presence.join(ws, topic, platform) | Add user to topic (call from subscribe hook) |
presence.leave(ws, platform) | Remove from all topics (call from close hook) |
presence.sync(ws, topic, platform) | Send list without joining (for observers) |
presence.list(topic) | Current user data array |
presence.count(topic) | Unique user count |
presence.clear() | Reset everything (stops heartbeat timer) |
Client API
import { presence } from 'svelte-adapter-uws/plugins/presence/client';
const users = presence('room');
// $users = [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }] The presence() function accepts an optional second argument with a maxAge option (in milliseconds). When set, entries that haven’t been refreshed within that window are automatically removed from the store. This makes clients self-healing when the server fails to broadcast leave events under load.
Important: maxAge requires the server-side heartbeat option. Without heartbeat, no events arrive between the initial list and eventual leave, so maxAge would expire every user - including ones who are still connected. The heartbeat periodically tells clients which keys are still active, resetting their maxAge timers.
// Server: heartbeat every 60s
const presence = createPresence({ key: 'id', heartbeat: 60_000 });
// Client: entries expire after 120s without a heartbeat refresh
const users = presence('room', { maxAge: 120_000 }); Rule of thumb: set heartbeat to half (or less) of the client’s maxAge.
Client example
<!-- src/routes/room/+page.svelte -->
<script>
import { on } from 'svelte-adapter-uws/client';
import { presence } from 'svelte-adapter-uws/plugins/presence/client';
const messages = on('room');
const users = presence('room');
</script>
<aside>
<h3>{$users.length} online</h3>
{#each $users as user (user.id)}
<span>{user.name}</span>
{/each}
</aside> SSR with presence
Use presence.list() in load functions:
// +page.server.js
import { presence } from '$lib/server/presence';
export async function load() {
return { users: presence.list('room'), online: presence.count('room') };
} How multi-tab dedup works
If user “Alice” (key id: '1') has three browser tabs open, presence.join() is called three times with the same key. The plugin ref-counts connections per key: Alice appears once in the list. When she closes two tabs, she stays present. Only when the last tab closes does the plugin broadcast a leave event.
If Alice’s data changes between connections (for example she updates her avatar in one session and opens a fresh tab), join() detects the difference and broadcasts an updated event so other clients immediately see the new data. The updated event has the same shape as join: { key, data }.
If no key field is found in the selected data (e.g. no auth), each connection is tracked separately.
Limitations
- In-memory only. Same as replay - server restart clears presence. On restart, clients reconnect and re-subscribe, so the list rebuilds within seconds.
- Single-worker only. Each worker tracks its own presence. In clustered mode, the list reflects only the local worker’s connections.
- Requires subscription. The client must subscribe to the topic (via
on(),crud(), etc.) for the server’ssubscribehook to fire.presence('room')alone shows you the list but doesn’t register you as present unless you’re also subscribed toroom.
Was this page helpful?