Packages
@surfjs/vue
Vue 3 composables for Surf — local commands, real-time state, channels, and Surf Live
@surfjs/vue
Vue 3 composables wrapping @surfjs/web. Register local command handlers, subscribe to Surf Live events, manage channels, and sync state — all with Vue reactivity.
Installation#
npm install @surfjs/vue@surfjs/web is included as a dependency — no separate install needed.
Peer dependencies: vue ^3.3.0
SurfProvider#
Manages WebSocket connection and provides context to child components via Vue's provide/inject:
<template> <SurfProvider url="wss://myapp.com/surf/ws" endpoint="https://myapp.com" auth="your-token" :channels="['project-123']" > <App /> </SurfProvider></template> <script setup lang="ts">import { SurfProvider } from '@surfjs/vue'</script>Props
| Prop | Type | Description |
|------|------|-------------|
| url | string | WebSocket URL for Surf Live (required) |
| endpoint | string? | Server URL for HTTP fallback. Also registers window.surf |
| auth | string? | Auth token sent on connect |
| channels | string[]? | Surf Live 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, SurfProvider 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.
useSurf#
Access the Surf context (requires SurfProvider):
<script setup lang="ts">import { useSurf } from '@surfjs/vue' const { execute, status, connected, sessionId } = useSurf() async function search(query: string) { const result = await execute('search', { query }) if (result.ok) { console.log(result.result) }}</script> <template> <div> <span>Status: {{ status }}</span> <button :disabled="!connected" @click="search('laptop')"> Search </button> </div></template>Returns
| Property | Type | Description |
|----------|------|-------------|
| execute | (command: string, params?: Record<string, unknown>) => Promise<SurfResult> | Execute a command via WebSocket |
| status | Ref<ConnectionStatus> | Current connection status |
| connected | ComputedRef<boolean> | Whether the WebSocket is connected |
| sessionId | Ref<string \| undefined> | Current session ID, if active |
| 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 |
| channels | Ref<ReadonlySet<string>> | Currently subscribed channels |
Throws if used outside SurfProvider.
useSurfEvent#
Subscribe to Surf Live events. Automatically cleans up on unmount:
<script setup lang="ts">import { useSurfEvent } from '@surfjs/vue' useSurfEvent('notification', (data) => { const { message, level } = data as { message: string; level: string } showToast(message, level)}) useSurfEvent('timeline.updated', (data) => { refreshTimeline()})</script>Parameters
| Param | Type | Description |
|-------|------|-------------|
| event | string | Event name to listen for |
| callback | (data: unknown) => void | Called when the event fires |
useSurfChannel#
Manage channel subscriptions dynamically:
<script setup lang="ts">import { useSurfChannel } from '@surfjs/vue' const { subscribe, unsubscribe, channels } = useSurfChannel() function joinProject(id: string) { subscribe(`project-${id}`)} function leaveProject(id: string) { unsubscribe(`project-${id}`)}</script> <template> <div>Active channels: {{ channels.size }}</div></template>Returns
| Property | Type | Description |
|----------|------|-------------|
| subscribe | (channelId: string) => void | Subscribe to a channel |
| unsubscribe | (channelId: string) => void | Unsubscribe from a channel |
| channels | Ref<ReadonlySet<string>> | Currently subscribed channels |
useSurfState#
Reactive state synced from Surf Live events. Returns a Vue ref that auto-updates when the server sends surf:state or surf:patch events:
<script setup lang="ts">import { useSurfState } from '@surfjs/vue' interface Metrics { users: number events: number latency: number} const metrics = useSurfState<Metrics>('metrics', { users: 0, events: 0, latency: 0,})</script> <template> <div class="dashboard"> <span>Online: {{ metrics.users }}</span> <span>Events: {{ metrics.events }}</span> <span>Latency: {{ metrics.latency }}ms</span> </div></template>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 Ref<T>.
useSurfCommands#
Register local command handlers with window.surf. Handlers run in the browser — no server roundtrip for mode: 'local' commands:
<script setup lang="ts">import { useSurfCommands } from '@surfjs/vue'import { useCanvasStore } from '@/stores/canvas' const store = useCanvasStore() useSurfCommands({ 'theme.toggle': { mode: 'local', run: () => { document.documentElement.classList.toggle('dark') return { ok: true } }, }, 'canvas.addCircle': { mode: 'local', run: (params) => { store.addCircle(params as { x: number; y: number; radius: number }) return { ok: true } }, }, 'doc.save': { mode: 'sync', run: (params) => { store.saveLocal(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 SurfProvider — registers directly with window.surf via @surfjs/web. Handlers are registered on mount and cleaned up on unmount.
SurfBadge#
A floating badge indicating the site is Surf-enabled. Helps AI vision models discover your commands:
<template> <SurfBadge endpoint="https://myapp.com" name="My App" :commands="commands" position="bottom-left" theme="auto" /></template> <script setup lang="ts">import { SurfBadge, type SurfBadgeCommand } from '@surfjs/vue' const commands: SurfBadgeCommand[] = [ { name: 'search', description: 'Search products' }, { name: 'cart.add', description: 'Add item to cart' },]</script>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| endpoint | string | — | Server URL (required) |
| name | string? | — | App name shown on hover |
| description | string? | — | App description |
| commands | SurfBadgeCommand[] | [] | Commands to advertise |
| position | 'bottom-right' \| 'bottom-left' \| 'inline' | 'bottom-left' | Badge placement |
| theme | 'dark' \| 'light' \| 'auto' | 'auto' | Colour scheme |
| className | string? | — | Additional CSS class |
If no SurfProvider is wrapping the badge, it automatically registers window.surf with an HTTP-only server executor via the endpoint prop.
SSR Considerations#
Nuxt / SSR frameworks
All composables and components are safe for SSR — they guard against window and WebSocket access on the server:
SurfProvider— WebSocket connection andwindow.surfregistration only run insideonMounted(client-side)useSurfCommands— CallsregisterCommandfrom@surfjs/web, which no-ops whenwindowis unavailableuseSurfState— Returns theinitialStateon the server; hydrates reactively on the clientSurfBadge— Renders hidden until mounted (visibility: hiddenuntil client-side)
No special configuration needed. Just use the composables as normal in your Nuxt pages or SSR Vue apps.
Lazy-loading with defineAsyncComponent
For apps that don't need Surf on every page:
<script setup lang="ts">import { defineAsyncComponent } from 'vue' const SurfProvider = defineAsyncComponent(() => import('@surfjs/vue').then(m => m.SurfProvider))</script>TypeScript#
All exports are fully typed. Key types you can import:
import type { // Context & connection SurfContextValue, ConnectionStatus, // 'connecting' | 'connected' | 'disconnected' | 'reconnecting' SurfResult, EventCallback, // Commands SurfCommandConfig, // = CommandConfig from @surfjs/web SurfCommandsMap, // Record<string, SurfCommandConfig> // Badge SurfBadgeProps, SurfBadgeCommand,} from '@surfjs/vue'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: Ref<ConnectionStatus> connected: ComputedRef<boolean> sessionId: Ref<string | undefined> subscribe: (event: string, callback: EventCallback) => () => void subscribeChannel: (channelId: string) => void unsubscribeChannel: (channelId: string) => void channels: Ref<ReadonlySet<string>>}Re-exports from @surfjs/web#
For convenience, @surfjs/vue re-exports the core @surfjs/web functions so you don't need a separate import:
import { initSurf, ensureSurf, registerCommand, unregisterCommand, getSurf, destroySurf,} from '@surfjs/vue'See @surfjs/web for details on these functions.