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.

WhatWhereSame cookies?
Page load+page.server.js load()Yes
Form action+page.server.js actionsYes
API route+server.jsYes
Server hookhooks.server.js handle()Yes
WebSocket upgradehooks.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
}
PropertyTypeDescription
headersobjectAll request headers, lowercased keys
cookiesobjectParsed cookies from the Cookie header
urlstringThe request path
remoteAddressstringClient 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.).

HookPurposeDeny 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:

MethodDescription
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?