Getting Started
Architecture & Execution Models
How window.surf works as a local runtime, the three execution modes, and strategies for where commands run
Architecture & Execution Models#
Surf is not a typical API framework. Commands can execute in the browser, on the server, or both ā and the manifest tells agents which path each command takes.
This page is the mental model for building with Surf. Read it once and you'll know where every piece fits.
The Surf Execution Stack#
Every Surf interaction enters through one of these layers:
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā Browser Agent āā āā window.surf.execute() ā local handlers ā ā Primary: instant, privateāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤ā Headless Agent āā āā SurfClient.execute() ā HTTP POST ā ā SDK for programmatic useāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤ā Developer āā āā surf test <url> <cmd> ā ā CLI for dev/debugāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤ā Server āā āā command.run(params, ctx) ā ā Server-side handlersāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤ā Surf Live (WebSocket) āā āā State broadcast ā connected clients ā ā Real-time sync layerāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāBrowser agents are the primary use case. They navigate to your site, discover window.surf, and execute commands ā seeing results as real UI changes, not JSON blobs.
Headless agents use @surfjs/client for programmatic access over HTTP. No browser needed.
Developers use the CLI to inspect manifests and test commands during development. It's a dev tool, not an agent interface.
Package Architecture#
Surf is split into focused packages across two runtimes:
Browser Runtime @surfjs/web ā Core: window.surf, handler registry, dispatcher āā @surfjs/react ā React hooks (useSurfCommands, SurfProvider, useSurfState) āā @surfjs/vue ā Future: Vue composables āā @surfjs/svelte ā Future: Svelte storesĀ Server Runtime @surfjs/core ā Commands, manifest, middleware, Surf Live āā Express / Fastify / Hono / Next adapters (built-in)Ā Agents & Tools @surfjs/client ā Headless SDK (HTTP + WebSocket) @surfjs/cli ā Developer tool (ping, inspect, test)@surfjs/web is the browser runtime. It owns window.surf, the local handler registry, and the dispatch logic. It's framework-agnostic ā you can use it with React, Vue, Svelte, or plain JavaScript.
@surfjs/react (and future framework packages) are thin lifecycle wrappers. useSurfCommands is React sugar for @surfjs/web's registerCommand. SurfProvider manages WebSocket setup via React context. All the actual runtime work happens in @surfjs/web.
@surfjs/core is the server runtime. It defines commands, generates the manifest, handles HTTP execution, and manages Surf Live WebSocket connections.
window.surf ā The Local Runtime
This is the key insight: window.surf is not an HTTP wrapper. It's a local execution runtime with a handler registry, provided by @surfjs/web.
When a browser agent calls window.surf.execute('canvas.addCircle', { x: 100 }), here's what actually happens:
window.surf.execute('canvas.addCircle', { x: 100 }) ā ā¼ āā Local handler registered? āā ā ā YES NO ā ā ā¼ ā¼ Run local handler HTTP POST to server (instant, in-browser) (network round-trip) ā ā ā¼ ā¼ UI updates immediately Wait for response ā ā¼ (if mode: 'sync') Background sync to server ā ā¼ Surf Live broadcasts to other clientsLocal handlers are registered via @surfjs/web's registerCommand (or useSurfCommands in React). They run in the browser process ā no network, no latency, no server needed. The UI updates instantly because the handler modifies local state directly.
This is what makes Surf fundamentally different from an API: commands can execute where the UI lives.
Execution Modes#
Every command in the manifest includes an execution hint that tells agents where it runs:
| Value | window.surf | HTTP / CLI | When to use |
|-------|---------------|------------|-------------|
| "any" (default) | ā
Local handler + optional server sync | ā
Server handler | Works everywhere ā most commands |
| "browser" | ā
Local handler only | ā Returns error | UI-only, modifies live browser state |
| "server" | ā
Proxies to server | ā
Server handler | Needs auth, DB, or business logic |
execution: "any" ā the default
The command has a server-side handler and may also have a local handler. Works from browser, headless, or CLI.
// Server config'product.search': { description: 'Search products', hints: { execution: 'any', idempotent: true }, run: async ({ query }) => { return db.products.search(query) }}execution: "browser" ā local only
The command only makes sense in a browser context. It modifies live UI state ā no server involved.
// Server config ā declaration only, no run() needed'canvas.addCircle': { description: 'Add circle to canvas', params: { x: { type: 'number', required: true }, y: { type: 'number', required: true }, radius: { type: 'number', default: 50 }, fill: { type: 'string', default: '#000' }, }, hints: { execution: 'browser', sideEffects: true }, // No run() ā browser-only command // The actual handler lives client-side via useSurfCommands}// Client ā registers the local handlerimport { useSurfCommands } from '@surfjs/react'Ā function Canvas() { useSurfCommands({ 'canvas.addCircle': { mode: 'local', run: (params) => { const circle = canvasStore.addCircle(params) return { ok: true, result: circle } } } })Ā return <CanvasRenderer />}When a headless agent or CLI tries to call a browser-only command:
$ surf test myapp.com canvas.addCircle --x 400ā ļø canvas.addCircle requires browser execution Open the site and run: await window.surf.execute('canvas.addCircle', { x: 400 })execution: "server" ā always server-side
The command needs server resources: database, auth, external APIs. window.surf proxies it to the server transparently.
'order.create': { description: 'Place an order', hints: { execution: 'server', sideEffects: true }, run: async (params, ctx) => { const order = await db.orders.create(ctx.auth.userId, params) ctx.live?.setState(`order:${order.id}`, order) return order }}Three Command Strategies#
How you implement commands depends on where state lives. There are three patterns:
1. Server-Authoritative
Commands modify the backend. The browser reflects changes via Surf Live or page reload.
Best for: E-commerce, CMS, SaaS ā anything with a database as source of truth.
// Serverconst surf = await createSurf({ commands: { 'cart.add': { description: 'Add item to cart', params: { sku: { type: 'string', required: true }, quantity: { type: 'number', default: 1 }, }, hints: { execution: 'any', sideEffects: true }, run: async ({ sku, quantity }, ctx) => { const item = await db.cart.add(ctx.sessionId, sku, quantity) const cart = await db.cart.get(ctx.sessionId)Ā // Broadcast to connected browsers ctx.live?.setState(`cart:${ctx.sessionId}`, cart)Ā return { added: item, total: cart.items.length } }, }, },})Agent calls cart.add ā Server writes to DB ā Surf Live ā Browser cart updates2. Client-Side / Local-First
Commands modify browser state only. No server round-trip. Instant feedback.
Best for: Design tools, creative apps, local-first apps, anything where the browser IS the application.
// Client ā the entire command stack lives in the browserimport { useSurfCommands } from '@surfjs/react'import { useCanvasStore } from './store'Ā function CanvasApp() { const canvasStore = useCanvasStore()Ā useSurfCommands({ 'canvas.addCircle': { mode: 'local', run: ({ x, y, radius, fill }) => { const circle = canvasStore.addCircle({ x, y, radius, fill }) return { ok: true, result: circle } } }, 'canvas.clear': { mode: 'local', run: () => { canvasStore.clear() return { ok: true } } }, 'canvas.getState': { mode: 'local', run: () => { return { ok: true, result: canvasStore.getSnapshot() } } }, })Ā return <CanvasRenderer store={canvasStore} />}// Server ā declares commands for discovery, no handlers'canvas.addCircle': { description: 'Add circle to canvas', params: { x: { type: 'number' }, y: { type: 'number' }, radius: { type: 'number' }, fill: { type: 'string' } }, hints: { execution: 'browser', sideEffects: true },},'canvas.clear': { description: 'Clear the canvas', hints: { execution: 'browser', sideEffects: true },},'canvas.getState': { description: 'Get current canvas state', hints: { execution: 'browser' },},Agent calls canvas.addCircle ā Local handler runs ā Canvas updates instantly (0ms)3. Hybrid / Sync
Local handler for instant UI, background sync to server. Best of both worlds.
Best for: Collaborative editors, dashboards, real-time apps where local speed matters but server persistence is needed.
import { useSurfCommands } from '@surfjs/react'import { useDocStore } from './store'Ā function CollaborativeEditor() { const docStore = useDocStore()Ā useSurfCommands({ 'doc.insert': { mode: 'sync', // Execute locally, then sync to server run: ({ position, text }) => { docStore.insertLocal(position, text) return { ok: true } // After returning, Surf automatically POSTs to server // Server broadcasts via Surf Live to other clients } }, 'doc.save': { mode: 'sync', run: (params) => { docStore.updateLocal(params) return { ok: true } } }, 'doc.getContent': { mode: 'local', // Pure read, no sync needed run: () => { return { ok: true, result: docStore.getContent() } } }, })Ā return <Editor store={docStore} />}Agent calls doc.insert ā Local handler (instant UI) ā Background POST ā Server persists ā Surf Live ā Other clients updateThe Identity Stack#
Sessions, auth, and execution modes form a coherent stack:
| Layer | Identity | Persistence | Use case | |-------|----------|-------------|----------| | No auth | Anonymous | None | Public reads, search | | Surf sessions | Transient | Server memory | Carts, wizards, multi-step flows | | Bearer token | Authenticated | Permanent | Purchases, admin, user-specific data | | Browser cookies | Browser session | Browser-managed | Traditional web auth |
Execution modes interact with auth naturally:
execution: "browser"commands don't need auth ā they modify local stateexecution: "server"commands often require auth ā they modify backend stateexecution: "any"commands may or may not need auth depending on the operation
The Feedback Principle#
Every Surf command should result in a visible change.
This is the core design principle. If an agent executes a command and nothing happens in the UI, the integration is incomplete.
| Strategy | How feedback works | |----------|--------------------| | Server-authoritative | Server handler ā Surf Live broadcast ā browser UI updates | | Client-side / Local-first | Local handler ā direct state mutation ā UI updates instantly | | Hybrid / Sync | Local handler ā instant UI ā server sync ā Surf Live ā other clients |
window.surf makes this possible because local handlers modify the UI directly. There's no waiting for a server response to know something happened ā the change is immediate.
Decision Guide#
| Your app | Strategy | execution hint | Implementation |
|----------|----------|-------------------|----------------|
| Has a database as source of truth | Server-authoritative | "any" or "server" | run() on server, Surf Live for feedback |
| Browser IS the app (canvas, editor) | Client-side | "browser" | useSurfCommands with mode: 'local' |
| Needs instant UI + server persistence | Hybrid | "any" | useSurfCommands with mode: 'sync' |
| Multiple clients need live sync | Server + Surf Live | "any" | run() on server, useSurfState on client |
| Not sure? | Server-authoritative | "any" | Simplest, works for most apps |
Most apps start server-authoritative. Add local handlers with useSurfCommands when you need instant feedback. Use mode: 'sync' when you need both.