Skip to content

Getting Started

How It Works

The three-step Surf protocol: discover, execute, see results — browser-first

How It Works#

Surf follows a three-step protocol: discover, execute, feedback. What makes it different is that all three steps can happen entirely in the browser.

1. Discovery

An agent needs to find out what your site can do. There are two discovery paths:

Browser agent (primary) — navigates to your site and checks window.surf:

JavaScript
// Agent opens your site in a browser, then:
const manifest = await window.surf.manifest()
console.log(manifest.commands)
// => {
// search: { description: 'Search products', hints: { execution: 'any' } },
// 'canvas.addCircle': { description: 'Add circle', hints: { execution: 'browser' } },
// 'order.create': { description: 'Place order', hints: { execution: 'server' } }
// }

window.surf is the primary discovery signal. It's provided by @surfjs/web — the framework-agnostic browser runtime — and registered automatically when your app uses SurfProvider or SurfBadge (@surfjs/react), or calls initSurf() directly. The manifest includes execution hints so agents know what each command can do and where it runs.

Headless agent — fetches the manifest over HTTP:

TypeScript
GET /.well-known/surf.json
JSON
{
"name": "My Store",
"commands": {
"search": {
"description": "Search products",
"params": { "query": { "type": "string", "required": true } },
"hints": { "execution": "any", "idempotent": true }
}
}
}

The manifest includes a SHA-256 checksum for drift detection via ETag/304, so agents can cache it efficiently.

2. Execution

Once an agent knows what's available, it calls commands. There are three paths:

Browser agent — via window.surf (instant for local commands):

JavaScript
// Local command — executes in-browser, no server round-trip
const result = await window.surf.execute('canvas.addCircle', { x: 100, y: 200 })
// => { ok: true, result: { id: 'c1', x: 100, y: 200 } }
// Canvas updates instantly — the handler ran locally
 
// Server command — proxied through to the server transparently
const order = await window.surf.execute('order.create', { sku: 'LAPTOP-01' })
// => { ok: true, result: { orderId: '...' } }

window.surf (powered by @surfjs/web) checks for a local handler first. If one exists (registered via useSurfCommands in React or registerCommand in vanilla JS), it runs immediately in the browser. If not, it falls back to an HTTP POST to the server.

Headless agent — via @surfjs/client:

TypeScript
const client = await SurfClient.discover('https://shop.example.com')
const result = await client.execute('search', { query: 'laptop' })

Developer — via CLI (for testing during development):

Bash
surf test https://shop.example.com search --query "laptop"

Both headless and CLI paths go through HTTP. They can only reach server-side handlers — execution: "browser" commands are not available over HTTP.

3. Feedback

This is where Surf separates from APIs. The agent doesn't just get JSON — it sees the result:

  • Local commands update the UI directly. The agent executed canvas.addCircle and a circle appeared on screen. No waiting.
  • Server commands broadcast changes via Surf Live (WebSocket). The agent executed cart.add and the cart icon updated with a new count.
  • Hybrid commands do both: instant local update, then background sync to server, then Surf Live broadcast to other connected clients.

Every command should result in a visible change. That's the feedback principle — and it's what makes Surf feel like interaction rather than API calls.

Browser-First, Not API-First#

Surf is designed for agents that interact with websites in a browser. window.surf is the primary interface. The HTTP API exists for headless/programmatic use, but the browser experience is first-class.

This matters because:

| | API-first | Surf (browser-first) | |---|-----------|---------------------| | Discovery | Read API docs | window.surf.manifest() — live, on the page | | Execution | HTTP request → wait → parse JSON | Local handler → instant UI change | | Verification | Parse response, hope it worked | See the change happen on screen | | Context | None — stateless request | Full browser context — cookies, DOM, visual state |

Browser-first doesn't mean browser-only. Headless agents and the CLI work fine over HTTP. But the design target is an agent sitting in a browser, interacting with a live application.

What Makes Surf Different#

Surf is not "another API framework." APIs are server-to-server. Surf commands can execute in the browser, modifying live UI state.

Consider a canvas drawing app:

JavaScript
// API approach: POST to server, get JSON back, somehow update the UI
fetch('/api/canvas/addCircle', { method: 'POST', body: JSON.stringify({ x: 100, y: 200 }) })
// => { "id": "c1" } ...now what? The UI didn't change.
 
// Surf approach: execute locally, UI updates instantly
await window.surf.execute('canvas.addCircle', { x: 100, y: 200 })
// => Circle appears on canvas. Done.

The agent interacts with the running application, not a backend endpoint. window.surf is a local runtime — commands registered via useSurfCommands execute in the same process as the UI. There's no network hop for local commands.

What Happens on the Server#

Server-side commands execute a run() function — what it does is up to you. See Architecture & Execution Models for the three strategies:

  1. Server-authoritative — command writes to your database, browser updates via Surf Live
  2. Client-side / Local-first — command runs entirely in the browser via useSurfCommands
  3. Hybrid / Sync — instant local execution + background server sync