Skip to content

Framework Adapters

Hono

Hono adapter for edge runtimes and Cloudflare Workers

Hono#

Surf provides first-class Hono support via @surfjs/core. The honoApp and honoMiddleware functions use the Web Streams API for SSE, making them compatible with Cloudflare Workers, Deno, Bun, and any edge runtime.

Bash
pnpm add @surfjs/core hono

Mount Surf as a Hono sub-app. Note that honoApp is async — always await it:

TypeScript
// server.ts
import { Hono } from 'hono'
import { createSurf, honoApp } from '@surfjs/core'
 
const surf = await createSurf({
name: 'My API',
commands: {
search: {
description: 'Search items',
params: {
query: { type: 'string', required: true },
limit: { type: 'number', default: 10 },
},
run: async ({ query, limit }) => {
return db.items.search(query, { limit })
},
},
},
})
 
const app = new Hono()
app.route('/', await honoApp(surf))
 
export default app

Cloudflare Worker

For Cloudflare Workers, use honoMiddleware to get a fetch handler. Again, honoMiddleware is async — use top-level await or initialize in a startup phase:

TypeScript
// worker.ts
import { createSurf, honoMiddleware } from '@surfjs/core'
 
const surf = await createSurf({
name: 'Edge API',
commands: {
ping: {
description: 'Health check',
run: async () => ({ pong: true, region: 'auto' }),
},
},
})
 
// Returns a standard fetch handler
export default { fetch: await honoMiddleware(surf) }

Sync Variant (No Dynamic Import)

If you need synchronous initialization — useful in environments where top-level await is constrained — pass the Hono constructor directly using honoAppSync:

TypeScript
import { Hono } from 'hono'
import { createSurf } from '@surfjs/core'
import { honoAppSync } from '@surfjs/core/hono'
 
const surf = await createSurf({ name: 'My API', commands: { /* ... */ } })
 
// Pass the Hono constructor directly — no dynamic import, fully synchronous
const surfSubApp = honoAppSync(surf, Hono)
const app = new Hono()
app.route('/', surfSubApp)
 
export default app

💡 Tip: honoApp is async because it dynamically imports Hono. honoAppSync avoids this by accepting the Hono constructor directly — useful in environments where top-level await is constrained or dynamic imports are problematic.

Composing with Existing Hono Routes

Surf mounts under the root path. You can freely add other routes to your Hono app:

TypeScript
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { createSurf } from '@surfjs/core'
import { honoApp } from '@surfjs/core'
 
const surf = await createSurf({ /* ... */ })
 
const app = new Hono()
 
// Your existing middleware
app.use('*', cors())
 
// Your existing routes
app.get('/health', (c) => c.json({ ok: true }))
app.get('/api/v1/users', (c) => getUsersHandler(c))
 
// Mount Surf routes alongside your API
app.route('/', await honoApp(surf))
 
export default app

With Authentication

Pass an authVerifier to createSurf. The Hono adapter extracts the Authorization header automatically and populates ctx.claims:

TypeScript
import { authFailed } from '@surfjs/core'
 
const surf = await createSurf({
name: 'Protected API',
auth: { type: 'bearer', description: 'API token' },
authVerifier: async (token, command) => {
const user = await db.users.findByToken(token)
if (!user) return { valid: false, reason: 'Invalid token' }
return { valid: true, claims: { userId: user.id, role: user.role } }
},
commands: {
'profile.get': {
description: 'Get the authenticated user profile',
auth: 'required',
run: async (_params, ctx) => {
return db.users.findById(ctx.claims!.userId)
},
},
'admin.stats': {
description: 'Server statistics (admin only)',
auth: 'required',
run: async (_params, ctx) => {
if (ctx.claims!.role !== 'admin') {
throw authFailed('Admin access required')
}
return db.getStats()
},
},
},
})

Endpoints

The Hono adapter mounts the following routes:

| Method | Path | Description | |--------|------|-------------| | GET | /.well-known/surf.json | Manifest (with ETag/304 caching) | | POST | /surf/execute | Command execution (supports SSE streaming) | | POST | /surf/pipeline | Pipeline execution | | POST | /surf/session/start | Start a session | | POST | /surf/session/end | End a session |

Edge Runtime Notes

| Feature | Cloudflare Workers | Deno / Bun | |---------|-------------------|------------| | Manifest + Discovery | ✅ | ✅ | | Command execution | ✅ | ✅ | | SSE Streaming | ✅ (Web Streams) | ✅ | | WebSocket + Surf Live | ⚠️ Requires custom implementation | ✅ | | Sessions | ✅ (KV or D1 store) | ✅ |

💡 Tip: Surf's built-in WebSocket transport uses the ws npm package, which is not available in Cloudflare Workers. For WebSocket support on Workers, you'd need a custom implementation using Durable Objects.