Skip to content

Getting Started

Deployment

Deploy your Surf API to production — Vercel, Cloudflare Workers, Node.js, and Docker

Deployment

Surf runs anywhere JavaScript runs. This guide covers deploying to the most common platforms with production-ready configuration.

Vercel (Next.js)#

The fastest path to production with @surfjs/next.

Setup

Bash
npm install @surfjs/core @surfjs/next
TypeScript
// app/api/surf/[...surf]/route.ts
import { createSurf } from '@surfjs/core'
import { createSurfRouteHandler } from '@surfjs/next'
 
const surf = await createSurf({
name: 'My API',
commands: {
search: {
description: 'Search products',
params: { query: { type: 'string', required: true } },
run: async ({ query }) => db.search(query),
},
},
})
 
export const { GET, POST } = createSurfRouteHandler(surf)

Deploy

Bash
vercel --prod

Vercel automatically detects Next.js and deploys as serverless functions. No additional configuration needed.

Edge Runtime

To run on Vercel Edge Functions, add the runtime directive:

TypeScript
// app/api/surf/[...surf]/route.ts
export const runtime = 'edge'
 
// ... same setup as above
// @surfjs/core uses Web Crypto API — fully edge-compatible

Environment Variables

Set via the Vercel dashboard or CLI:

Bash
vercel env add SURF_AUTH_SECRET
vercel env add DATABASE_URL

Cloudflare Workers#

Using @surfjs/web with Hono for the lightest edge deployment.

Setup

Bash
npm install @surfjs/core hono
TypeScript
// src/index.ts
import { createSurf, honoApp } from '@surfjs/core'
import { Hono } from 'hono'
import { cors } from 'hono/cors'
 
const surf = await createSurf({
name: 'Edge API',
commands: {
search: {
description: 'Search products',
params: { query: { type: 'string', required: true } },
run: async ({ query }, ctx) => {
// Use Workers KV, D1, or any edge-native storage
return { results: [] }
},
},
},
})
 
const app = new Hono()
app.use('/*', cors())
app.route('/', await honoApp(surf))
 
export default app

wrangler.toml

TOML
name = "my-surf-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"
 
[vars]
SURF_AUTH_SECRET = "set-in-dashboard"

Deploy

Bash
npx wrangler deploy

Secrets

Bash
npx wrangler secret put SURF_AUTH_SECRET
npx wrangler secret put DATABASE_URL

Node.js (Express / Fastify / Hono)#

For traditional server deployments. See the individual adapter docs for full setup:

Express Example

TypeScript
// server.ts
import { createSurf } from '@surfjs/core'
import express from 'express'
import cors from 'cors'
 
const surf = await createSurf({
name: 'My API',
commands: { /* ... */ },
})
 
const app = express()
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }))
app.use(express.json())
app.use(surf.middleware())
 
const port = parseInt(process.env.PORT || '3000')
app.listen(port, () => {
console.log(`Surf API running on port ${port}`)
})

Build & Run

Bash
# TypeScript build
npx tsc
 
# Run
node dist/server.js
 
# Or with tsx for development
npx tsx src/server.ts

Process Manager (PM2)

For production Node.js deployments:

Bash
npm install -g pm2
JavaScript
// ecosystem.config.js
module.exports = {
apps: [{
name: 'surf-api',
script: 'dist/server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
}],
}
Bash
pm2 start ecosystem.config.js
pm2 save
pm2 startup

Docker#

Dockerfile

dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]

.dockerignore

TypeScript
node_modules
.git
.env
dist
Dockerfile
docker-compose.yml

docker-compose.yml

YAML
services:
surf-api:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
- SURF_AUTH_SECRET=${SURF_AUTH_SECRET}
- DATABASE_URL=${DATABASE_URL}
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/.well-known/surf.json"]
interval: 30s
timeout: 5s
retries: 3

Build & Run

Bash
docker compose up -d

Environment Variables#

Common environment variables across all deployment targets:

| Variable | Description | Required | |----------|-------------|----------| | PORT | Server port (Node.js/Docker) | No (default: 3000) | | NODE_ENV | Environment (production / development) | Recommended | | SURF_AUTH_SECRET | Secret for auth token verification | If using auth | | DATABASE_URL | Database connection string | If using a database | | ALLOWED_ORIGINS | Comma-separated CORS origins | Recommended |

.env File (Development)

Bash
PORT=3000
SURF_AUTH_SECRET=dev-secret-change-in-prod
DATABASE_URL=postgresql://localhost:5432/mydb
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000

Never commit .env files. Add .env to .gitignore and use platform-specific secret management in production.

Production Checklist#

Before going live, verify each item:

CORS

Configure CORS to allow only your frontend origins:

TypeScript
import cors from 'cors'
 
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
methods: ['GET', 'POST'],
credentials: true,
}))

For Hono:

TypeScript
import { cors } from 'hono/cors'
 
app.use('/*', cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
}))

Rate Limiting

Protect your API from abuse:

TypeScript
// Express with express-rate-limit
import rateLimit from 'express-rate-limit'
 
app.use('/surf/execute', rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per window
standardHeaders: true,
}))

For Cloudflare Workers, use Cloudflare Rate Limiting at the edge.

Authentication

If your commands require auth, configure it at the Surf level:

TypeScript
const surf = await createSurf({
name: 'My API',
auth: { type: 'bearer', description: 'API token' },
authVerifier: async (token) => {
const valid = token === process.env.SURF_AUTH_SECRET
return valid
? { valid: true, claims: { role: 'user' } }
: { valid: false, reason: 'Invalid token' }
},
commands: { /* ... */ },
})

See the Authentication guide for token generation, scoped auth, and more.

HTTPS

Always serve over HTTPS in production:

  • Vercel / Cloudflare — HTTPS by default
  • Node.js behind a proxy — Use nginx or Caddy as a reverse proxy with TLS
  • Docker — Terminate TLS at the load balancer or reverse proxy

Health Check

The Surf manifest endpoint doubles as a health check:

Bash
curl https://myapi.com/.well-known/surf.json

Returns 200 with the manifest if the server is healthy.

Logging

Add request logging for observability:

TypeScript
// Express
import morgan from 'morgan'
app.use(morgan('combined'))
 
// Hono
import { logger } from 'hono/logger'
app.use(logger())

Summary

| Item | Status | |------|--------| | CORS configured | ☐ | | Rate limiting enabled | ☐ | | Auth secrets set via env vars | ☐ | | HTTPS enabled | ☐ | | Health check working | ☐ | | Logging configured | ☐ | | .env not committed | ☐ | | Error handling in place | ☐ |