hooks.ws.js
The WebSocket hooks file at src/hooks.ws.js (or .ts) controls the WebSocket lifecycle. It’s required for svelte-realtime to work.
Minimal setup
// src/hooks.ws.js
import { createMessage } from 'svelte-realtime/server';
export function upgrade() {
return { id: crypto.randomUUID() };
}
export const message = createMessage(); One-call setup with realtime()
Since 0.5.6, the realtime() factory returns the standard hook set in one call - the recommended path for cluster deployments because the same declaration of bus / leader intent reaches every framework publish surface (RPC, cron, reactive seam, top-level publish()) in lockstep.
// src/hooks.ws.js
import { realtime } from 'svelte-realtime/server';
export const { open, close, message, init } = realtime();
export function upgrade() { return { id: crypto.randomUUID() }; } For cluster mode pass { bus, leader } - see Distributed Pub/Sub.
Available hooks
upgrade({ cookies })
Runs on every new WebSocket connection. Return user data to attach, or false to reject.
export function upgrade({ cookies }) {
const session = validateSession(cookies.session_id);
if (!session) return false;
return { id: session.userId, name: session.name };
} authenticate({ cookies, ... })
Optional. Runs as a normal HTTP POST /__ws/auth before every WebSocket upgrade (including reconnects). cookies.set() becomes Set-Cookie on a standard 204 response, which proxies route correctly. Use this when you need to refresh a session cookie on connect behind Cloudflare Tunnel or any edge proxy that strips Set-Cookie from 101 Switching Protocols responses.
export function authenticate({ cookies }) {
const session = validateSession(cookies.get('session_id'));
if (!session) return false; // -> 401, client does not open the WebSocket
if (shouldRotate(session)) {
cookies.set('session_id', rotate(session), {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/'
});
}
return { id: session.userId, name: session.name };
} The hook receives the SvelteKit event shape ({ request, headers, cookies, url, remoteAddress, getClientAddress, platform }). Return undefined for success, false for 401, or a full Response for full control. Requires svelte-adapter-uws >= 0.4.12 and configure({ auth: true }) on the client.
See Cloudflare-Tunnel cookie fix for the full diagnosis and end-to-end setup.
message
Routes incoming WebSocket messages to your src/live/ functions. Create it with createMessage():
import { createMessage } from 'svelte-realtime/server';
export const message = createMessage({
async beforeExecute(ws, rpcPath) {
// Rate limiting, logging, etc.
}
}); open(ws, { platform })
Fires when a WebSocket connection is fully established.
subscribe(ws, topic, ctx)
Fires when a client subscribes to a stream topic.
unsubscribe(ws, topic, ctx)
Fires when a client’s subscription count for a topic reaches zero.
close(ws, ctx)
Fires when the WebSocket closes (tab closed, network drop, etc).
Custom message handling
When you need to mix RPC with custom WebSocket messages, use onUnhandled or drop to handleRpc directly.
onJsonMessage
Added in svelte-realtime 0.5.9 + svelte-adapter-uws 0.5.3. Receives non-RPC text frames that parsed as a JSON object - the recommended dispatcher for plugin frames (cursor, presence, custom protocols using {type: '...'} envelopes).
import { createMessage } from 'svelte-realtime/server';
import { cursors } from '$lib/server/cursors';
export const message = createMessage({
onJsonMessage(ws, msg, platform) {
if (msg.type === 'cursor') cursors.hooks.message(ws, { data: msg, platform });
}
}); Two-tier lookup: the fast path uses the msg field forwarded by svelte-adapter-uws@^0.5.3 (one parse total); the fallback parses locally for frames the adapter didn’t fast-path (older adapter, > 8 KiB frame, or non-{"ty prefix). Frames that aren’t JSON, can’t parse, parse to a non-object, or exceed maxJsonDepth (default 64) fall through to onUnhandled with the original raw bytes.
Bench: ~7x faster than re-parsing in onUnhandled at cursor scale (1000 movers x 60 Hz). Use onJsonMessage for JSON-shaped plugin envelopes and onUnhandled for binary frames or anything else.
onUnhandled
Pass onUnhandled to createMessage to handle non-RPC messages (binary data, custom protocols, parse failures, frames past maxJsonDepth):
export const message = createMessage({
onUnhandled(ws, data, platform) {
// handle non-RPC messages (binary data, custom protocols, etc.)
}
}); Both onJsonMessage and onUnhandled can be set together for mixed JSON / binary frame handling.
handleRpc
For full control, use handleRpc directly and handle non-RPC messages yourself:
import { handleRpc } from 'svelte-realtime/server';
export function message(ws, { data, platform }) {
if (handleRpc(ws, data, platform)) return;
// your custom message handling
} handleRpc returns true if the message was an RPC call, false otherwise. This lets you branch on the result and handle everything else however you want.
Progression
export { message } → createMessage({...}) → manual handleRpc. Start simple, add options when needed, drop to full control only if you have to.
Was this page helpful?