Adding Surf to a Next.js App
Use Next.js Route Handlers to expose typed Surf commands. Works with the App Router, edge runtime, and deploys instantly to Vercel โ no extra infrastructure needed.
Overview
Surf lives entirely in your API layer. For a Next.js app this means a single Route Handler that mounts your Surf router. Agents discover your commands via /.well-known/surf.json and call them over plain HTTP โ no WebSocket server, no long-running process, no extra Vercel project.
What you add
app/api/surf/[...surf]/route.tsapp/.well-known/surf.json/route.tsWhat you get
Installation
Install the core package into your existing Next.js project:
npm install surfjs# orpnpm add surfjsThat's the only dependency. Surf has no peer dependencies and ships ESM + CJS.
Route Handler
Create a catch-all Route Handler. The [...surf] segment lets Surf handle all sub-paths for commands, pipelines, and streaming.
import { createSurf } from 'surfjs'import { NextRequest } from 'next/server'ย // 1. Define your commandsconst surf = createSurf({ name: 'My Next.js App', baseUrl: process.env.NEXT_PUBLIC_BASE_URL!, commands: { search: { description: 'Search articles or products', parameters: { q: { type: 'string', required: true, description: 'Search query' }, limit: { type: 'number', default: 10 }, }, async handler({ q, limit }) { // query your DB, CMS, or search index const results = await db.search(q, { limit }) return { results, total: results.length } }, },ย page: { description: 'Get structured content for any page', parameters: { slug: { type: 'string', required: true }, }, async handler({ slug }) { const page = await cms.getPage(slug) if (!page) throw new Error('Page not found') return { title: page.title, content: page.body, meta: page.meta } }, }, },})ย // 2. Export the handler โ Next.js handles routingconst handler = surf.toNextHandler()export { handler as GET, handler as POST }Commands are now reachable at POST /api/surf/search and POST /api/surf/page. Agents execute them by name โ no URL mapping required.
surf.json Manifest
Surf auto-generates the manifest at /api/surf/.well-known/surf.json, but convention is to expose it at the root. Add a tiny proxy route:
import { NextResponse } from 'next/server'ย export async function GET() { const manifest = await fetch( `${process.env.NEXT_PUBLIC_BASE_URL}/api/surf/.well-known/surf.json` ).then(r => r.json()) return NextResponse.json(manifest)}ย // Ensure this route is never cached โ agents always need the live schemaexport const dynamic = 'force-dynamic'Now any AI agent can discover your commands with a single curl:
curl https://yourapp.vercel.app/.well-known/surf.jsonMiddleware & Auth
Surf has a first-class middleware system. Use it to add authentication, logging, or per-tenant rate limiting before commands run:
import { createSurf } from 'surfjs'import { verifyToken } from '@/lib/auth'ย const surf = createSurf({ name: 'My Next.js App', baseUrl: process.env.NEXT_PUBLIC_BASE_URL!,ย // Global auth middleware โ runs before every command middleware: [ async (ctx, next) => { const token = ctx.request.headers.get('Authorization')?.replace('Bearer ', '') if (!token) throw new Error('Unauthorized')ย const user = await verifyToken(token) ctx.state.user = user // available in all handlers via ctx.state return next() }, ],ย // Per-command rate limiting rateLimit: { window: '1m', max: 60, // 60 requests / min per IP },ย commands: { search: { description: 'Search (auth required)', parameters: { q: { type: 'string', required: true } }, async handler({ q }, ctx) { const { user } = ctx.state return db.search(q, { tenantId: user.tenantId }) }, }, },})ย const handler = surf.toNextHandler()export { handler as GET, handler as POST }Edge Runtime
Surf is edge-compatible by default โ no Node.js-only APIs. To opt into the edge runtime and eliminate cold starts on Vercel, add one line to your route file:
// Add this export at the top of your route fileexport const runtime = 'edge'ย // The rest of your Surf setup is unchanged...import { createSurf } from 'surfjs'ย const surf = createSurf({ /* ... */ })const handler = surf.toNextHandler()export { handler as GET, handler as POST }Commands served from the edge have sub-10ms latency globally โ well below Surf's 47ms average. The constraint: your handlers must use edge-compatible APIs (fetch, Web Crypto, KV stores โ not fs or native modules).
Deploy to Vercel
No extra config needed. Push to your repo and Vercel auto-deploys. Make sure to set your base URL env var:
# Set via Vercel CLIvercel env add NEXT_PUBLIC_BASE_URL production# โ https://yourapp.vercel.appย # Or in the Vercel dashboard under Project โ Settings โ Environment VariablesAfter deploy, verify your manifest is live:
curl https://yourapp.vercel.app/.well-known/surf.json | jq .commandsPreview deployments
Vercel creates a unique URL per branch โ useful for testing Surf command changes before merging. Set NEXT_PUBLIC_BASE_URL to the preview URL in your preview environment, or use VERCEL_URL in a server component to derive it at runtime.
Summary
Adding Surf to a Next.js app comes down to three files:
app/api/surf/[...surf]/route.tsโ Your commands + Surf routerapp/.well-known/surf.json/route.tsโ Manifest proxy for agent discovery.env.localโ NEXT_PUBLIC_BASE_URL=https://yourapp.vercel.appFrom there, every AI agent that discovers your surf.json can call your commands directly โ no screenshots, no brittle selectors, no vision models.