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:

typescript
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ 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:

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

typescript
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 clients

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

typescript
// 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.

typescript
// 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
}
tsx
// Client — registers the local handler
import { 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:

typescript
$ 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.

typescript
'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.

typescript
// Server
const 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 }
},
},
},
})
typescript
Agent calls cart.add → Server writes to DB → Surf Live → Browser cart updates

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

tsx
// Client — the entire command stack lives in the browser
import { 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} />
}
typescript
// 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' },
},
typescript
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.

tsx
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} />
}
typescript
Agent calls doc.insert → Local handler (instant UI) → Background POST → Server persists → Surf Live → Other clients update

The 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 state
  • execution: "server" commands often require auth — they modify backend state
  • execution: "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.

This site speaks Surf
AI agents can read and interact with this site through structured commands — no scraping needed.
9 things agents can do
Search all documentation pagesGet the full documentation treeGet raw content of a doc pageList all @surfjs npm packagesGet details about a specific packageLatest release details+3 more
Surf ProtocolLearn more →