Rooms - live.room()

Rooms bundle data, presence, cursors, and scoped actions into a single declaration. They are the high-level primitive for collaborative features where multiple related streams share the same topic scope.

Declaring a room

// src/live/collab.js
import { live } from 'svelte-realtime/server';

export const board = live.room({
  topic: (ctx, boardId) => 'board:' + boardId,
  init: async (ctx, boardId) => db.cards.forBoard(boardId),
  presence: (ctx) => ({ name: ctx.user.name, avatar: ctx.user.avatar }),
  cursors: true,
  guard: async (ctx) => {
    if (!ctx.user) throw new LiveError('UNAUTHORIZED');
  },
  actions: {
    addCard: async (ctx, boardId, title) => {
      const card = await db.cards.insert({ boardId, title });
      ctx.publish('created', card);
      return card;
    }
  }
});
OptionDescription
topicDynamic topic function → receives (ctx, ...args) and returns the topic string
initAsync function that returns the initial data for the room
presenceFunction returning the presence payload for the current user
cursorsSet to true to enable cursor tracking
guardOptional guard function → runs before any room action or subscription
actionsObject of named async functions scoped to this room

Using a room on the client

On the client, the room export becomes an object with sub-streams and actions. Room actions receive the same leading arguments as the topic function (boardId in this case), followed by any action-specific arguments:

<script>
  import { board } from '$live/collab';

  const data = board.data(boardId);         // main data stream
  const users = board.presence(boardId);     // presence stream
  const cursors = board.cursors(boardId);    // cursor stream
</script>

{#each $data as card (card.id)}
  <Card {card} />
{/each}

<button onclick={() => board.addCard(boardId, 'New card')}>Add</button>

Each sub-stream (data, presence, cursors) is a standard Svelte store. They follow the same lifecycle as regular streams → undefined while loading, data when ready, { error } on failure.

Room hooks shortcut

Rooms expose a .hooks property for one-liner wiring in hooks.ws.js:

// src/hooks.ws.js
import { board } from './live/collab.js';

export const { message, close, unsubscribe } = board.hooks;

This exports the message, close, and unsubscribe hooks pre-wired for the room’s streams and lifecycle.

Cluster-correct presence

live.room({ presence }) keeps the per-room roster in a per-process Map by default, which is the right thing in single-process dev. In a multi-replica deployment the per-process roster diverges: a subscriber on replica A and a subscriber on replica B each see only the joiners they share a process with, and a refresh on a replica that has no other connected subscribers re-renders the list to “1 online” (self only).

Wire platform.redis in your hooks.ws.js init and the framework switches live.room({ presence }) to a cluster-shared Redis HASH (__live-presence:{topic}). Join and leave transitions are refcounted across replicas - only the first replica to take a user’s count from 0 to 1 publishes join, only the last to take it from 1 to 0 publishes leave - so tab-flip and multi-replica reconnects do not flicker the roster.

// src/hooks.ws.js
import { createRedisClient } from 'svelte-adapter-uws-extensions/redis';

const redis = createRedisClient({ url: process.env.REDIS_URL });

export async function init({ platform }) {
  platform.redis = redis.redis;
}

platform.redis is the raw ioredis-shaped client (the .redis property of the wrapper). The framework calls hincrby / hset / hdel / hgetall / expire on it directly. If platform.redis is unset, the in-process Map fallback stays in place - so the zero-config dev path keeps working without changes.

The same pattern lights up cluster correctness for related primitives in the ecosystem - see the Redis presence extension for the standalone presence plugin and Scaling guide for the full multi-replica wiring.


Rooms combine Streams, Merge Strategies (presence, cursor, crud), and Auth into a single cohesive unit. See those pages for details on each underlying primitive.

Was this page helpful?