Framework Adapters
SvelteKit
SvelteKit integration via server hooks and @surfjs/svelte
SvelteKit#
SvelteKit uses Vite and exposes server hooks and +server.ts route handlers — both work well with Surf. Use @surfjs/core on the server and @surfjs/svelte on the client.
pnpm add @surfjs/core @surfjs/svelteServer Setup
Define Your Surf Instance
Create a shared Surf instance in a server module:
// src/lib/server/surf.tsimport { createSurf } from '@surfjs/core' export const surf = await createSurf({ name: 'My SvelteKit App', commands: { search: { description: 'Search products', params: { query: { type: 'string', required: true }, limit: { type: 'number', default: 10 }, }, run: async ({ query, limit }) => { return db.products.search(query, { limit }) }, }, },})Catch-All Route Handler
Create a catch-all +server.ts that delegates to Surf's middleware:
// src/routes/surf/[...path]/+server.tsimport { surf } from '$lib/server/surf'import type { RequestHandler } from './$types' const handler = surf.middleware() function handleSurf(event: Parameters<RequestHandler>[0]): Promise<Response> { return new Promise((resolve) => { const { request } = event // Build a minimal Node-like req/res pair from the web Request const url = new URL(request.url) const req = { method: request.method, url: url.pathname + url.search, headers: Object.fromEntries(request.headers.entries()), on: (_event: string, cb: (chunk: Buffer) => void) => { request.text().then((body) => { if (body) cb(Buffer.from(body)) }) }, } const chunks: Buffer[] = [] const resHeaders: Record<string, string> = {} let statusCode = 200 const res = { statusCode, setHeader(name: string, value: string) { resHeaders[name] = value }, getHeader(name: string) { return resHeaders[name] }, writeHead(code: number, headers?: Record<string, string>) { statusCode = code if (headers) Object.assign(resHeaders, headers) }, write(chunk: string | Buffer) { chunks.push(Buffer.from(chunk)) }, end(chunk?: string | Buffer) { if (chunk) chunks.push(Buffer.from(chunk)) resolve(new Response(Buffer.concat(chunks), { status: statusCode, headers: resHeaders, })) }, } handler(req as never, res as never, () => { resolve(new Response(JSON.stringify({ error: 'Not Found' }), { status: 404, headers: { 'Content-Type': 'application/json' }, })) }) // Feed the request body request.text().then((body) => { if (body) { req.on('data', () => {}) } }) })} export const GET: RequestHandler = (event) => handleSurf(event)export const POST: RequestHandler = (event) => handleSurf(event)💡 Tip: For a cleaner approach, use the Hono adapter with SvelteKit's
@sveltejs/adapter-node— Hono uses Web Standard Request/Response natively, avoiding the Node.js compatibility shim above.
Discovery Endpoint
Add the /.well-known/surf.json route for agent discovery:
// src/routes/.well-known/surf.json/+server.tsimport { surf } from '$lib/server/surf'import type { RequestHandler } from './$types' export const GET: RequestHandler = async () => { const manifest = surf.manifest() return new Response(JSON.stringify(manifest), { headers: { 'Content-Type': 'application/json' }, })}Endpoints
With the routes above, your SvelteKit app exposes:
| Method | Path | Description |
|--------|------|-------------|
| GET | /.well-known/surf.json | Manifest (discovery) |
| POST | /surf/execute | Command execution |
| POST | /surf/pipeline | Pipeline execution |
| POST | /surf/session/start | Start session |
| POST | /surf/session/end | End session |
Client-Side Integration
Use @surfjs/svelte stores in your Svelte components.
Provider Setup
Initialize the Surf context in your root layout:
<!-- src/routes/+layout.svelte --><script> import { createSurfProvider, setSurfContext } from '@surfjs/svelte' const provider = createSurfProvider({ endpoint: 'https://myapp.com', }) setSurfContext(provider)</script> <slot />Registering Local Commands
<!-- src/routes/+page.svelte --><script> import { surfCommands, surfExecute } from '@surfjs/svelte' // Register local commands for browser-based agents surfCommands({ 'theme.toggle': { mode: 'local', run: () => { document.documentElement.classList.toggle('dark') return { ok: true } }, }, }) // Execute server commands let results = $state(null) async function search(query) { results = await surfExecute('search', { query }) }</script>Real-Time State with Surf Live
Add WebSocket support to the provider:
<script> import { createSurfProvider, setSurfContext } from '@surfjs/svelte' const provider = createSurfProvider({ endpoint: 'https://myapp.com', url: 'wss://myapp.com/surf/ws', }) setSurfContext(provider)</script>Then subscribe to live state:
<script> import { surfState } from '@surfjs/svelte' const metrics = surfState('metrics', { users: 0, events: 0 })</script> <div>Online: {$metrics.users}</div>With Authentication
Pass auth tokens through the provider:
<script> import { createSurfProvider, setSurfContext } from '@surfjs/svelte' import { page } from '$app/stores' const provider = createSurfProvider({ endpoint: 'https://myapp.com', auth: $page.data.token, }) setSurfContext(provider)</script>Alternative: Hono on SvelteKit
For a more ergonomic server-side setup, use the Hono adapter with SvelteKit. Hono uses Web Standard APIs natively, eliminating the Node.js compatibility layer:
// src/hooks.server.tsimport { Hono } from 'hono'import { honoApp } from '@surfjs/core/hono'import { surf } from '$lib/server/surf' const app = new Hono()app.route('/', await honoApp(surf)) export const handle = async ({ event, resolve }) => { const response = await app.fetch(event.request) if (response.status !== 404) return response return resolve(event)}Deployment Notes
| Feature | Vercel / Cloudflare | Node.js Server | |---------|--------------------|--------------------| | Manifest + Discovery | ✅ | ✅ | | Command execution | ✅ | ✅ | | Pipelines + Sessions | ✅ | ✅ | | SSE Streaming | ✅ | ✅ | | WebSocket + Surf Live | ❌ | ✅ |
⚠️ Surf Live requires a persistent server process. Use
@sveltejs/adapter-nodeand deploy to Railway, Fly.io, or a VPS for WebSocket support.