Framework Adapters
Next.js
First-class Next.js App Router support
Next.js#
The @surfjs/next package provides first-class Next.js App Router support with edge-compatible Web Standard Request/Response handling. Install it alongside @surfjs/core:
pnpm add @surfjs/next @surfjs/coreRoute Handler
Create a catch-all route handler at app/api/surf/[[...slug]]/route.ts:
// app/api/surf/[[...slug]]/route.tsimport { createSurf } from '@surfjs/core'import { createSurfRouteHandler } from '@surfjs/next'ย const surf = await createSurf({ name: 'My Next.js App', commands: { search: { description: 'Search products', params: { query: { type: 'string', required: true } }, run: async ({ query }) => db.search(query), }, },})ย // Returns { GET, POST } โ export directly as Next.js route handlersexport const { GET, POST } = createSurfRouteHandler(surf)You can also pass a custom basePath option:
export const { GET, POST } = createSurfRouteHandler(surf, { basePath: '/api/surf', // default})Discovery Middleware
Surf discovery relies on /.well-known/surf.json at the domain root, but Next.js catch-all routes live under /api/surf/. The surfMiddleware handles this automatically:
// middleware.ts (project root)import { surfMiddleware } from '@surfjs/next/middleware'ย export default surfMiddleware()ย export const config = { matcher: ['/.well-known/surf.json'],}This rewrites /.well-known/surf.json โ /api/surf, making surf ping https://your-app.com work out of the box.
Alternative: next.config.js Rewrites
If you prefer not to use middleware, you can use Next.js rewrites instead โ simpler, zero-code:
// next.config.jsmodule.exports = { async rewrites() { return [ { source: '/.well-known/surf.json', destination: '/api/surf/.well-known/surf.json' }, ] },}Both approaches work equally well. Use middleware if you need to compose with other middleware logic; use rewrites if you want simplicity.
Custom base path
If your Surf routes are mounted somewhere other than /api/surf:
export default surfMiddleware({ basePath: '/api/v2/surf' })Composing with existing middleware
import { surfMiddleware } from '@surfjs/next/middleware'import { NextResponse, type NextRequest } from 'next/server'ย const surf = surfMiddleware()ย export default function middleware(request: NextRequest) { // Surf discovery rewrite const surfResponse = surf(request) if (surfResponse) return surfResponseย // ... your other middleware logic return NextResponse.next()}ย export const config = { matcher: ['/.well-known/surf.json', '/dashboard/:path*'],}Endpoints
This mounts the following endpoints:
| Method | Path | Description |
|--------|------|-------------|
| GET | /.well-known/surf.json | Manifest (via middleware) |
| GET | /api/surf | Manifest (direct) |
| POST | /api/surf/execute | Command execution |
| POST | /api/surf/pipeline | Pipeline execution |
| POST | /api/surf/session/start | Start session |
| POST | /api/surf/session/end | End session |
Deployment Notes
| Feature | Serverless (Vercel) | Persistent server (Railway, Fly.io, VPS) | |---------|-------|--------| | Manifest + Discovery | โ | โ | | Command execution | โ | โ | | Pipelines + Sessions | โ | โ | | SSE Streaming | โ | โ | | WebSocket + Surf Live | โ | โ |
โ ๏ธ Surf Live (real-time state sync via WebSocket) requires a persistent server process. Vercel serverless functions do not support WebSocket connections. Deploy to Railway, Fly.io, a VPS, or any platform with persistent Node.js processes for Surf Live features.