Skip to content

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:

Bash
pnpm add @surfjs/next @surfjs/core

Route Handler

Create a catch-all route handler at app/api/surf/[[...slug]]/route.ts:

TypeScript
// app/api/surf/[[...slug]]/route.ts
import { 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 handlers
export const { GET, POST } = createSurfRouteHandler(surf)

You can also pass a custom basePath option:

TypeScript
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:

TypeScript
// 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:

JavaScript
// next.config.js
module.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:

TypeScript
export default surfMiddleware({ basePath: '/api/v2/surf' })

Composing with existing middleware

TypeScript
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.