Packages
@surfjs/web
Framework-agnostic browser runtime — window.surf, local handler registry, and command dispatcher
@surfjs/web
The core browser runtime for Surf. Provides window.surf, the local handler registry, and the command dispatcher. Framework-agnostic — works with React, Vue, Svelte, or plain JavaScript.
If you're using React, you probably want
@surfjs/react— it wraps@surfjs/webwith React hooks and lifecycle management. Everything on this page still applies, but you won't call these functions directly.
Installation#
npm install @surfjs/webArchitecture#
@surfjs/web is the foundation of Surf's browser runtime:
@surfjs/web ← You are here ├─ @surfjs/react ← React hooks (useSurfCommands, SurfProvider) ├─ @surfjs/vue ← Vue composables (useSurfCommands, SurfProvider) └─ @surfjs/svelte ← Svelte stores (surfCommands, surfState)Framework packages are thin lifecycle wrappers. @surfjs/react's useSurfCommands calls registerCommand and cleans up on unmount. SurfProvider calls initSurf and manages WebSocket setup via React context. The actual runtime — window.surf, handler dispatch, server fallback — all lives here.
initSurf
Initialize the window.surf runtime:
import { initSurf } from '@surfjs/web' initSurf({ endpoint: 'https://myapp.com', // Server URL for manifest + fallback ws: 'wss://myapp.com/surf/ws', // Optional: WebSocket for Surf Live auth: 'your-token', // Optional: auth token})After calling initSurf(), window.surf is available globally. Agents can call window.surf.execute(), window.surf.manifest(), etc.
| Option | Type | Description |
|--------|------|-------------|
| endpoint | string | Server URL for manifest fetch and HTTP fallback |
| ws | string? | WebSocket URL for Surf Live connection |
| auth | string? | Auth token sent with requests |
registerCommand
Register a local command handler:
import { registerCommand } from '@surfjs/web' registerCommand('canvas.addCircle', { mode: 'local', run: (params) => { const circle = canvasStore.addCircle(params) return { ok: true, result: circle } }})When window.surf.execute('canvas.addCircle', ...) is called, the local handler runs immediately — no network request.
Modes
| Mode | Behavior |
|------|----------|
| 'local' | Runs in browser only. No server contact. |
| 'sync' | Runs locally first, then POSTs to server in background. |
// Local-only — no serverregisterCommand('canvas.clear', { mode: 'local', run: () => { canvasStore.clear() return { ok: true } }}) // Local + background syncregisterCommand('doc.save', { mode: 'sync', run: (params) => { docStore.updateLocal(params) return { ok: true } // Surf automatically POSTs to server after returning }})unregisterCommand
Remove a local handler:
import { unregisterCommand } from '@surfjs/web' unregisterCommand('canvas.addCircle')In React,
useSurfCommandshandles registration and cleanup automatically on mount/unmount. You don't need to callunregisterCommandmanually.
destroySurf
Tear down the runtime and remove window.surf:
import { destroySurf } from '@surfjs/web' destroySurf()// window.surf is now undefinedFull Example — Vanilla JS#
<script type="module"> import { initSurf, registerCommand } from '@surfjs/web' // Initialize runtime initSurf({ endpoint: 'https://myapp.com' }) // Register local handlers registerCommand('theme.toggle', { mode: 'local', run: () => { document.body.classList.toggle('dark') const isDark = document.body.classList.contains('dark') return { ok: true, result: { dark: isDark } } } }) registerCommand('search', { mode: 'local', run: ({ query }) => { const results = searchIndex.search(query) return { ok: true, result: results } } }) // Agents can now use: // await window.surf.execute('theme.toggle') // await window.surf.execute('search', { query: 'laptop' })</script>Full Example — Vue#
<script setup>import { onMounted, onUnmounted } from 'vue'import { initSurf, registerCommand, unregisterCommand, destroySurf } from '@surfjs/web' onMounted(() => { initSurf({ endpoint: 'https://myapp.com' }) registerCommand('counter.increment', { mode: 'local', run: () => { count.value++ return { ok: true, result: { count: count.value } } } })}) onUnmounted(() => { unregisterCommand('counter.increment') destroySurf()})</script>How window.surf Dispatches
When window.surf.execute(command, params) is called:
- Local handler registered? → Run it immediately (0ms network)
- No local handler + WebSocket connected? → Send via WebSocket
- No local handler + no WebSocket? → HTTP POST to
{endpoint}/surf/execute
This means adding local handlers via registerCommand progressively upgrades window.surf from an HTTP client to a local runtime. Commands that don't have local handlers still work — they just go to the server.