Patterns

Isolated, self-contained recipes. Paste into your project and adapt.


Auth Guard

Reject unauthenticated users at the module level.

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

export const _guard = guard((ctx) => {
  if (!ctx.user?.id) throw new LiveError('UNAUTHORIZED', 'Login required');
});

export const secret = live(async (ctx) => {
  return db.secrets.forUser(ctx.user.id);
});

Optimistic Update

Update the UI immediately, roll back on error.

// src/live/todos.js
export const addTodo = live(async (ctx, text) => {
  const todo = await db.todos.insert({ text, userId: ctx.user.id });
  ctx.publish('todos', 'created', todo);
  return todo;
});

export const todos = live.stream('todos', async (ctx) => {
  return db.todos.forUser(ctx.user.id);
}, { merge: 'crud', key: 'id' });
<script>
  import { todos, addTodo } from '$live/todos';

  async function add(text) {
    const tempId = 'temp-' + Date.now();
    const rollback = todos.optimistic('created', { id: tempId, text, done: false });
    try {
      await addTodo(text);
    } catch {
      rollback();
    }
  }
</script>

Paginated List

Infinite scroll with loadMore() and cursors.

// src/live/feed.js
export const posts = live.stream('posts', async (ctx) => {
  const limit = 20;
  const rows = await db.posts.list({ limit: limit + 1, after: ctx.cursor });
  const hasMore = rows.length > limit;
  const data = hasMore ? rows.slice(0, limit) : rows;
  return { data, hasMore, cursor: data.at(-1)?.id ?? null };
}, { merge: 'crud', key: 'id' });
<script>
  import { posts } from '$live/feed';
</script>

{#each $posts as post (post.id)}
  <p>{post.title}</p>
{/each}

{#if posts.hasMore}
  <button onclick={() => posts.loadMore()}>Load more</button>
{/if}

Presence (Who’s Online)

Track connected users with lifecycle hooks.

// src/live/presence.js
export const online = live.stream('presence:lobby', () => [], {
  merge: 'presence',
  onSubscribe(ctx, topic) {
    ctx.publish(topic, 'join', { key: ctx.user.id, name: ctx.user.name });
  },
  onUnsubscribe(ctx, topic) {
    ctx.publish(topic, 'leave', { key: ctx.user.id });
  }
});
<script>
  import { online } from '$live/presence';
</script>

{#each $online as user (user.key)}
  <span>{user.name}</span>
{/each}

Typing Indicator

Ephemeral “user is typing” with presence merge.

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

export const typing = live.channel('typing:lobby', { merge: 'presence' });

export const setTyping = live((ctx) => {
  ctx.publish('typing:lobby', 'join', { key: ctx.user.id, name: ctx.user.name });
});

export const clearTyping = live((ctx) => {
  ctx.publish('typing:lobby', 'leave', { key: ctx.user.id });
});
<script>
  import { typing, setTyping, clearTyping } from '$live/typing';
</script>

{#each $typing as user (user.key)}
  <span>{user.name} is typing...</span>
{/each}

<input oninput={setTyping} onblur={clearTyping} />

Rate-Limited Broadcast

Throttle high-frequency publishes.

// src/live/cursor.js
export const moveCursor = live((ctx, x, y) => {
  ctx.throttle('cursors', 'update', {
    key: ctx.user.id, x, y, color: ctx.user.color
  }, 50);
});

export const cursors = live.stream(
  (ctx, docId) => 'cursors:' + docId,
  () => [],
  { merge: 'cursor' }
);
<script>
  import { moveCursor, cursors } from '$live/cursor';
  let { data } = $props();
  const positions = cursors(data.docId);
</script>

<svelte:window onmousemove={(e) => moveCursor(e.clientX, e.clientY)} />

{#each $positions as c (c.key)}
  <div style="left:{c.x}px;top:{c.y}px;background:{c.color}" class="cursor-dot" />
{/each}

Bulk Operations

Batch multiple publishes from one handler.

// src/live/admin.js
export const resetBoard = live(async (ctx, boardId) => {
  await db.boards.clearNotes(boardId);
  ctx.batch([
    { topic: `board:${boardId}:notes`, event: 'set', data: [] },
    { topic: `board:${boardId}:presence`, event: 'set', data: [] }
  ]);
});

File Upload (Binary)

Send files as ArrayBuffers via RPC.

// src/live/files.js
export const uploadFile = live.binary(async (ctx, buffer) => {
  const id = crypto.randomUUID();
  const ext = detectExtension(buffer);
  await storage.write(`${id}.${ext}`, buffer);
  return { id, size: buffer.byteLength };
});
<script>
  import { uploadFile } from '$live/files';
  let status = $state('');

  async function handle(e) {
    const file = e.target.files[0];
    status = 'Uploading...';
    const result = await uploadFile(await file.arrayBuffer());
    status = `Uploaded: ${result.id} (${result.size} bytes)`;
  }
</script>

<input type="file" onchange={handle} />
<p>{status}</p>

Error Boundary

Reusable component for stream loading/error states.

<!-- src/lib/StreamView.svelte -->
<script>
  let { store, children, loading, error } = $props();
  let value = $derived($store);
</script>

{#if value === undefined}
  {#if loading}{@render loading()}{:else}<p>Loading...</p>{/if}
{:else if value?.error}
  {#if error}{@render error(value.error)}{:else}<p>Error: {value.error.message}</p>{/if}
{:else}
  {@render children()}
{/if}
<script>
  import StreamView from '$lib/StreamView.svelte';
  import { messages } from '$live/chat';
</script>

<StreamView store={messages}>
  {#each $messages as msg (msg.id)}
    <p>{msg.text}</p>
  {/each}
</StreamView>

Multi-Room

Scope streams to dynamic topics.

// src/live/rooms.js
export const roomMessages = live.stream(
  (ctx, roomId) => 'chat:' + roomId,
  async (ctx, roomId) => db.messages.forRoom(roomId),
  { merge: 'crud', key: 'id' }
);

export const sendToRoom = live(async (ctx, roomId, text) => {
  const msg = await db.messages.insert({ roomId, userId: ctx.user.id, text });
  ctx.publish('chat:' + roomId, 'created', msg);
  return msg;
});
<script>
  import { roomMessages, sendToRoom } from '$live/rooms';
  let { data } = $props();
  const messages = roomMessages(data.roomId);
</script>

{#each $messages as msg (msg.id)}
  <p>{msg.text}</p>
{/each}

Cron Cleanup

Periodic server-side data pruning.

// src/live/maintenance.js
export const cleanup = live.cron('0 * * * *', 'items', async (ctx) => {
  const stale = await db.items.olderThan('24 hours');
  for (const item of stale) {
    await db.items.delete(item.id);
    ctx.publish('items', 'deleted', { id: item.id });
  }
});

Admin Broadcast

One-to-all notification from an admin panel.

// 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');
});

export const broadcast = live(async (ctx, message) => {
  ctx.publish('notifications', 'created', {
    id: crypto.randomUUID(),
    text: message,
    from: ctx.user.name,
    at: Date.now()
  });
});
// src/live/notifications.js (no guard - everyone can subscribe)
export const notifications = live.stream('notifications', () => [], {
  merge: 'latest', max: 50
});

Was this page helpful?