Skip to content

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:

TypeScript
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.

TypeScript
// 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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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

JSON
{ "type": "subscribe", "channels": ["project-123"] }

Unsubscribe

JSON
{ "type": "unsubscribe", "channels": ["project-123"] }

State Event

JSON
{
"type": "event",
"event": "surf:state",
"data": {
"channel": "project-123",
"state": { "timeline": { "clips": [], "playhead": 42.5 } },
"version": 7
}
}