Real-time
Surf Live
Real-time state sync between AI agents and your UI via WebSocket channels.
Surf Live
Surf Live enables real-time state broadcasting from your server to all connected browser clients. When an AI agent executes commands that change state, those changes automatically propagate to every connected UI — no polling, no manual refresh.
Use Cases#
- An AI agent editing a video timeline while the user watches live
- Collaborative editing where an agent makes changes visible to all viewers
- Real-time dashboards updated by agent workflows
Setup#
Enable Surf Live in your server config:
import { createSurf } from '@surfjs/core' const surf = await createSurf({ name: 'My App', commands: { /* your commands */ }, live: { enabled: true, maxChannelsPerConnection: 10, // Default: 10 allowedOrigins: ['https://myapp.com'], // CSRF protection (rejects unknown origins in production) maxPayloadBytes: 1_048_576, // Default: 1MB },})Channels#
A channel is a string identifier that groups connections. Clients subscribe to channels, and the server emits events scoped to those channels.
// In a command handler:surf.live.setState('project-123', { timeline: { clips: [...], playhead: 42.5 }, selectedClip: 'clip-7',})This emits a surf:state event to all clients subscribed to project-123.
State Methods#
setState(channelId, state)
Push full state to all subscribers on a channel:
surf.live.setState('project-123', { timeline: { clips: updatedClips, playhead: 42.5 },})patchState(channelId, patch)
Push a partial update — clients merge it into their current state via deep merge:
surf.live.patchState('project-123', { playhead: 43.0 })getState(channelId)
Get the last known state for a channel. Useful for initial delivery when a new client subscribes:
const current = surf.live.getState('project-123')if (current) { console.log(current.state, current.version)}// => { state: { timeline: {...} }, version: 7 }Returns { state: unknown; version: number } | undefined.
emit(event, data, channelId)
Emit a custom event to a channel:
surf.live.emit('cursor.moved', { x: 100, y: 200 }, 'project-123')Security#
Channel Auth
Optionally verify if a token has access to subscribe to a channel:
const surf = await createSurf({ // ... live: { enabled: true, channelAuth: async (token, channelId) => { const user = await verifyToken(token) return user.hasAccessTo(channelId) }, },})If channelAuth is configured, clients must authenticate before subscribing.
Limits
- Off by default — must set
live.enabled: true - Max channels per connection — default 10, configurable
- Isolation — channel events never leak to session-scoped listeners
Version Ordering#
Each setState and patchState call increments a version counter. Clients use this for ordering and deduplication — events with a version ≤ the last applied version are discarded.
WebSocket Protocol#
Subscribe
{ "type": "subscribe", "channels": ["project-123"] }Unsubscribe
{ "type": "unsubscribe", "channels": ["project-123"] }State Event
{ "type": "event", "event": "surf:state", "data": { "channel": "project-123", "state": { "timeline": { "clips": [], "playhead": 42.5 } }, "version": 7 }}