Packages
@surfjs/svelte
Svelte stores and actions for Surf — local commands, reactive state, and Surf Live
@surfjs/svelte
Svelte integration for Surf, wrapping @surfjs/web. Register local command handlers, sync state via stores, manage channels, and connect to Surf Live. Supports Svelte 4 and 5.
Installation#
npm install @surfjs/svelte@surfjs/web is included as a dependency — no separate install needed.
Peer dependencies: svelte ^4.0.0 || ^5.0.0
Provider Setup#
Unlike React/Vue, Svelte doesn't have a built-in component provider pattern. Instead, call createSurfProvider in your root component and pass it to children via Svelte's context API:
<!-- App.svelte --><script lang="ts"> import { createSurfProvider, setSurfContext } from '@surfjs/svelte' const provider = createSurfProvider({ url: 'wss://myapp.com/surf/ws', endpoint: 'https://myapp.com', auth: 'your-token', channels: ['project-123'], }) setSurfContext(provider)</script> <slot />CreateSurfProviderOptions
| Option | Type | Description |
|--------|------|-------------|
| url | string | WebSocket URL to connect to (required) |
| endpoint | string? | Server URL for HTTP fallback. Also registers window.surf |
| auth | string? | Auth token sent on connect |
| channels | string[]? | Channels to subscribe to on connect |
Auto-Reconnect
The provider automatically reconnects with exponential backoff: 1s → 2s → 4s → 8s → max 30s.
Channel subscriptions are restored on reconnect.
window.surf Integration
When endpoint is provided, createSurfProvider automatically initialises window.surf with a WebSocket-backed server executor. This means @surfjs/web functions like registerCommand route through the active WebSocket instead of HTTP fallback.
Cleanup
The provider returns a destroy() function for teardown:
<script lang="ts"> import { onDestroy } from 'svelte' import { createSurfProvider, setSurfContext } from '@surfjs/svelte' const provider = createSurfProvider({ url: 'wss://myapp.com/surf/ws', }) setSurfContext(provider) onDestroy(() => { provider.destroy() })</script>getSurfContext#
Access the Surf context in child components:
<script lang="ts"> import { getSurfContext } from '@surfjs/svelte' const { execute, status, subscribe } = getSurfContext() async function search(query: string) { const result = await execute('search', { query }) if (result.ok) { console.log(result.result) } }</script> <button on:click={() => search('laptop')}>Search</button>Returns
| Property | Type | Description |
|----------|------|-------------|
| execute | (command: string, params?: Record<string, unknown>) => Promise<SurfResult> | Execute a command via WebSocket |
| status | Writable<ConnectionStatus> | Current connection status store |
| subscribe | (event: string, cb: EventCallback) => () => void | Subscribe to Surf events |
| subscribeChannel | (channelId: string) => void | Subscribe to a channel |
| unsubscribeChannel | (channelId: string) => void | Unsubscribe from a channel |
| destroy | () => void | Tear down the connection |
Throws if used outside a component with setSurfContext.
Reactive Status
status is a Svelte writable store — use the $ prefix to read it reactively:
<script lang="ts"> import { getSurfContext } from '@surfjs/svelte' const { status } = getSurfContext()</script> {#if $status === 'connected'} <span class="badge green">Connected</span>{:else if $status === 'reconnecting'} <span class="badge yellow">Reconnecting…</span>{:else} <span class="badge red">Disconnected</span>{/if}surfCommands#
Register local command handlers with window.surf. Handlers run in the browser — no server roundtrip for mode: 'local' commands:
<script lang="ts"> import { surfCommands } from '@surfjs/svelte' surfCommands({ 'theme.toggle': { mode: 'local', run: () => { document.documentElement.classList.toggle('dark') return { ok: true } }, }, 'canvas.addCircle': { mode: 'local', run: (params) => { addCircleToCanvas(params as { x: number; y: number; radius: number }) return { ok: true } }, }, 'doc.save': { mode: 'sync', run: (params) => { saveLocally(params) return { ok: true } }, }, })</script>Handler Modes
| Mode | Behaviour |
|------|-----------|
| local | Runs only in the browser. No server call. |
| sync | Runs locally AND posts to server in the background for persistence. |
Works with or without the Surf context — registers directly with window.surf via @surfjs/web. Handlers are registered immediately and cleaned up via onDestroy.
surfState#
Reactive Svelte store synced from Surf Live events. Returns a writable store that auto-updates when the server sends surf:state or surf:patch events:
<script lang="ts"> import { surfState } from '@surfjs/svelte' interface Metrics { users: number events: number latency: number } const metrics = surfState<Metrics>('metrics', { users: 0, events: 0, latency: 0, })</script> <div class="dashboard"> <span>Online: {$metrics.users}</span> <span>Events: {$metrics.events}</span> <span>Latency: {$metrics.latency}ms</span></div>How it works
surf:state— Full state replacement for the given keysurf:patch— Deep merge of partial updates into existing state
Both carry a version number. Out-of-order messages with stale versions are ignored automatically.
Parameters
| Param | Type | Description |
|-------|------|-------------|
| key | string | State key (matches against channel name in events) |
| initialState | T | Initial value before any server events arrive |
Returns Writable<T>. Requires getSurfContext — must be used within a component that has setSurfContext.
surfExecute#
Convenience wrapper around window.surf.execute() for use without the provider context:
<script lang="ts"> import { surfExecute } from '@surfjs/svelte' async function search(query: string) { const result = await surfExecute('search', { query }) if (result.ok) { console.log(result.result) } }</script> <button on:click={() => search('laptop')}>Search</button>This calls ensureSurf() to guarantee window.surf exists, then delegates to the local handler registry. Useful when you've registered commands via surfCommands or set up window.surf with initSurf directly.
Dynamic Channels#
Manage channel subscriptions at runtime via the context:
<script lang="ts"> import { getSurfContext } from '@surfjs/svelte' const { subscribeChannel, unsubscribeChannel } = getSurfContext() function joinProject(id: string) { subscribeChannel(`project-${id}`) } function leaveProject(id: string) { unsubscribeChannel(`project-${id}`) }</script>SSR Considerations#
SvelteKit / SSR frameworks
All functions are safe for SSR — they guard against window and WebSocket access on the server:
createSurfProvider— WebSocket connection andwindow.surfregistration are wrapped intypeof window !== 'undefined'checks. On the server, the provider is created but no connection is established.surfCommands— CallsregisterCommandfrom@surfjs/web, which no-ops whenwindowis unavailable.surfState— Returns theinitialStateon the server; hydrates reactively on the client.surfExecute— Should only be called client-side (in event handlers,onMount, etc.).
No special configuration needed. Just use the functions as normal in your SvelteKit pages.
Client-only loading
For components that should only render on the client:
<!-- +page.svelte --><script lang="ts"> import { browser } from '$app/environment'</script> {#if browser} <SurfPanel />{/if}TypeScript#
All exports are fully typed. Key types you can import:
import type { // Provider CreateSurfProviderOptions, // Context & connection SurfContextValue, ConnectionStatus, // 'connecting' | 'connected' | 'disconnected' | 'reconnecting' SurfResult, EventCallback, // Commands SurfCommandConfig, // = CommandConfig from @surfjs/web SurfCommandsMap, // Record<string, SurfCommandConfig>} from '@surfjs/svelte'SurfResult
interface SurfResult { ok: boolean result?: unknown error?: { code: string; message: string } state?: Record<string, unknown>}SurfContextValue
interface SurfContextValue { execute: (command: string, params?: Record<string, unknown>) => Promise<SurfResult> status: Writable<ConnectionStatus> subscribe: (event: string, callback: EventCallback) => () => void subscribeChannel: (channelId: string) => void unsubscribeChannel: (channelId: string) => void destroy: () => void}Re-exports from @surfjs/web#
For convenience, @surfjs/svelte re-exports the core @surfjs/web functions so you don't need a separate import:
import { initSurf, ensureSurf, registerCommand, unregisterCommand, getSurf, destroySurf,} from '@surfjs/svelte'See @surfjs/web for details on these functions.