Auth

Connection auth - upgrade()

The upgrade function in src/hooks.ws.js runs on every new WebSocket connection. Return user data to attach to the connection, or false to reject it.

// src/hooks.ws.js
export function upgrade({ cookies }) {
  const session = validateSession(cookies.session_id);
  if (!session) return false;
  return { id: session.userId, name: session.name };
}

Whatever you return is available as ctx.user in all live() functions.

Per-module auth - guard()

Export a _guard from any src/live/ file. It runs before every function in that file.

// src/live/admin.js
import { live, guard, LiveError } from 'svelte-realtime/server';

export const _guard = guard((ctx) => {
  if (ctx.user?.role !== 'admin')
    throw new LiveError('FORBIDDEN', 'Admin only');
});

export const deleteUser = live(async (ctx, userId) => {
  await db.users.delete(userId);
});

Both functions in this file require admin access.

Composable guards

Chain multiple guards. They run in order, and earlier ones can enrich ctx for later ones:

export const _guard = guard(
  (ctx) => {
    if (!ctx.user) throw new LiveError('UNAUTHORIZED');
  },
  (ctx) => {
    ctx.permissions = lookupPermissions(ctx.user.id);
  },
  (ctx) => {
    if (!ctx.permissions.includes('write'))
      throw new LiveError('FORBIDDEN');
  }
);

Per-function auth

Check ctx.user inside any live() function:

export const deleteItem = live(async (ctx, id) => {
  if (!ctx.user) throw new LiveError('UNAUTHORIZED');
  if (ctx.user.role !== 'admin') throw new LiveError('FORBIDDEN');
  await db.items.delete(id);
});

Stream access control

Use the access option on live.stream() to control who can subscribe. The predicate receives ctx and is checked once at subscription time. If it returns false, the subscription is denied with { ok: false, code: 'FORBIDDEN', error: 'Access denied' } and no data is sent. For per-event filtering, use pipe.filter().

// Only admins can subscribe
export const adminFeed = live.stream('admin-feed', async (ctx) => {
  return db.adminEvents.recent();
}, {
  merge: 'crud',
  access: (ctx) => ctx.user?.role === 'admin'
});

// Role-based: different roles get different access
export const items = live.stream('items', async (ctx) => {
  return db.items.all();
}, {
  merge: 'crud',
  access: live.access.role({
    admin: true,
    viewer: false
  })
});

For per-user data isolation, use dynamic topics so each user subscribes to their own topic:

// Each user gets their own topic - no cross-user data leakage
export const myOrders = live.stream(
  (ctx) => `orders:${ctx.user.id}`,
  async (ctx) => db.orders.forUser(ctx.user.id),
  { merge: 'crud', key: 'id' }
);
HelperDescription
live.access.owner(field?)Subscription allowed if ctx.user[field] is present (default: 'id')
live.access.team()Subscription allowed if ctx.user.teamId is present
live.access.role(map)Role-based: { admin: true, viewer: (ctx) => ... }
live.access.any(...predicates)OR: any predicate returning true allows the subscription
live.access.all(...predicates)AND: all predicates must return true

Global middleware

Use live.middleware() for cross-cutting auth:

import { live, LiveError } from 'svelte-realtime/server';

live.middleware(async (ctx, next) => {
  if (!ctx.user) throw new LiveError('UNAUTHORIZED');
  return next();
});

Under the hood, upgrade() runs inside the adapter’s WebSocket handshake. See svelte-adapter-uws WebSocket docs for the full connection lifecycle.

Was this page helpful?