Authentication
WebSocket authentication uses the exact same cookies as your SvelteKit app. When the browser opens a WebSocket connection, it sends all cookies for the domain - including session cookies set by SvelteKit’s cookies.set(). No tokens, no query parameters, no extra client-side code.
Here’s the full flow from login to authenticated WebSocket:
Step 1: Login sets a cookie
Standard SvelteKit form action. Nothing WebSocket-specific here.
src/routes/login/+page.server.js
import { authenticate, createSession } from '$lib/server/auth.js';
export const actions = {
default: async ({ request, cookies }) => {
const form = await request.formData();
const email = form.get('email');
const password = form.get('password');
const user = await authenticate(email, password);
if (!user) return { error: 'Invalid credentials' };
const sessionId = await createSession(user.id);
// This cookie is automatically sent on WebSocket upgrade requests
cookies.set('session', sessionId, {
path: '/',
httpOnly: true,
sameSite: 'strict',
secure: true,
maxAge: 60 * 60 * 24 * 7 // 1 week
});
return { success: true };
}
}; Step 2: WebSocket handler reads the same cookie
The upgrade() function in hooks.ws.js receives parsed cookies from the upgrade request. Return an object to accept the connection (it becomes ws.getUserData()), or return false to reject with 401.
src/hooks.ws.js
import { getSession } from '$lib/server/auth.js';
export async function upgrade({ cookies }) {
// Same cookie that SvelteKit set during login
const sessionId = cookies.session;
if (!sessionId) return false; // -> 401, connection rejected
const user = await getSession(sessionId);
if (!user) return false; // -> 401, expired or invalid session
// Attach user data to the socket - available via ws.getUserData()
return { userId: user.id, name: user.name, role: user.role };
}
export function open(ws, { platform }) {
const { userId, role } = ws.getUserData();
console.log(`${userId} connected (${role})`);
// Subscribe to user-specific and role-based topics
ws.subscribe(`user:${userId}`);
if (role === 'admin') ws.subscribe('admin');
}
export function close(ws, { platform }) {
const { userId } = ws.getUserData();
console.log(`${userId} disconnected`);
} Step 3: Client - nothing special needed
The browser sends cookies automatically on the upgrade request. If the session is invalid, the connection is rejected and auto-reconnect will retry (useful if the user logs in later).
src/routes/dashboard/+page.svelte
<script>
import { on, status } from 'svelte-adapter-uws/client';
// The browser sends cookies automatically on the upgrade request.
// If the session is invalid, the connection is rejected and
// auto-reconnect will retry (useful if the user logs in later).
const notifications = on('notifications');
const userMessages = on('user-messages');
</script>
{#if $status === 'open'}
<span>Authenticated & connected</span>
{:else if $status === 'connecting'}
<span>Connecting...</span>
{:else}
<span>Disconnected (not logged in?)</span>
{/if} Step 4: Send messages to specific users
Because each user subscribes to their own user:{userId} topic in the open hook, you can send targeted messages from any server-side code via platform.publish().
src/routes/api/notify/+server.js
import { json } from '@sveltejs/kit';
export async function POST({ request, platform }) {
const { userId, message } = await request.json();
// Only that user receives this (they subscribed in open())
platform.publish(`user:${userId}`, 'notification', { message });
return json({ sent: true });
} Why this works
The WebSocket upgrade is an HTTP request. The browser treats it like any other request to your domain - it includes all cookies, follows the same-origin policy, and respects httpOnly/secure/sameSite flags. There’s no difference between how cookies reach a +page.server.js load function and how they reach the upgrade handler.
| What | Where | Same cookies? |
|---|---|---|
| Page load | +page.server.js load() | Yes |
| Form action | +page.server.js actions | Yes |
| API route | +server.js | Yes |
| Server hook | hooks.server.js handle() | Yes |
| WebSocket upgrade | hooks.ws.js upgrade() | Yes |
The upgrade() function
The upgrade function receives an UpgradeContext object:
{
headers: { 'cookie': '...', 'host': 'localhost:3000', ... }, // all lowercase
cookies: { session_id: 'abc123', theme: 'dark' }, // parsed from Cookie header
url: '/ws', // request path
remoteAddress: '127.0.0.1' // client IP
} | Property | Type | Description |
|---|---|---|
headers | object | All request headers, lowercased keys |
cookies | object | Parsed cookies from the Cookie header |
url | string | The request path |
remoteAddress | string | Client IP address |
Return an object to accept the connection → it becomes ws.getUserData() in all subsequent hooks. Return false to reject with a 401 response. Omit the upgrade export entirely to accept all connections.
The subscribe hook
The subscribe hook fires when a client tries to subscribe to a topic. Use it for topic-level authorization - deciding which users can access which topics.
export function subscribe(ws, topic, { platform }) {
const { role } = ws.getUserData();
// Only admins can subscribe to admin topics
if (topic.startsWith('admin') && role !== 'admin') return false;
} Return false to deny the subscription. Omit the hook to allow all subscriptions.
This is separate from connection-level auth in upgrade(). A user might be allowed to connect (valid session) but not allowed to subscribe to certain topics (wrong role, not a member of a room, etc.).
| Hook | Purpose | Deny by returning |
|---|---|---|
upgrade() | Connection-level auth (is this user logged in?) | false → 401 |
subscribe() | Topic-level auth (can this user access this topic?) | false → subscription silently blocked |
All hooks.ws hooks
The full set of hooks available in hooks.ws.js:
// Called during HTTP -> WebSocket upgrade.
// Return object to accept (becomes ws.getUserData()), false to reject.
export async function upgrade({ headers, cookies, url, remoteAddress }) { }
// Called when a connection is established
export function open(ws, { platform }) { }
// Called when a message is received
// subscribe/unsubscribe messages from the client store are handled
// automatically BEFORE this function is called
export function message(ws, { data, isBinary }) { }
// Called when a client tries to subscribe to a topic
// Return false to deny
export function subscribe(ws, topic, { platform }) { }
// Called when a client unsubscribes from a topic
export function unsubscribe(ws, topic, { platform }) { }
// Called when the connection closes
export function close(ws, { code, message, platform }) { }
// Called when backpressure has drained (for flow control)
export function drain(ws, { platform }) { } The ws object is a uWebSockets.js WebSocket with these key methods:
| Method | Description |
|---|---|
ws.getUserData() | Returns whatever upgrade() returned |
ws.subscribe(topic) | Subscribe to a topic |
ws.unsubscribe(topic) | Unsubscribe from a topic |
ws.send(data) | Send a message to this connection |
ws.close() | Close the connection |
Was this page helpful?