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?