Deploy
Vercel / Netlify / Cloudflare step-by-step.
docs/DEPLOY.mdAgentKit runs on any platform that supports Next.js 16. The hero chat route runs the live agent on the Node.js runtime (Node 22.12 or newer); everything else is static.
Vercel (recommended · 2 minutes)
bunx vercel
Vercel auto-detects Next.js 16. Follow the prompts:
- Link to your Vercel account (first time only).
- Select / create a project.
- Accept the default settings — Vercel figures it out.
Before you deploy, set showcase: false in site.config.ts. It hides the kit's own catalog (/components, /sections, /docs, /starters) so your site shows only your product.
After the first deploy, add the live-chat credentials:
Vercel dashboard → Your project → Settings → Environment Variables
OPENAI_API_KEY = sk-... (or ANTHROPIC_API_KEY / OPENROUTER_API_KEY, matching site.config.ts ai.provider)
(Production, Preview, Development)
TOOL_APPROVAL_SECRET = ... (openssl rand -hex 32; signs tool approvals)
UPSTASH_REDIS_REST_URL = https://...
UPSTASH_REDIS_REST_TOKEN = ...
Redeploy (Vercel does this automatically when you push, or click "Redeploy").
Custom domain
Project → Settings → Domains → Add → type your domain. Follow the DNS instructions.
Netlify (3 minutes)
# Ensure you have the Next.js plugin installed
bun add -d @netlify/plugin-nextjs
Create netlify.toml at the project root:
[build]
command = "bun run build"
publish = ".next"
[[plugins]]
package = "@netlify/plugin-nextjs"
Then:
bunx netlify deploy --prod
Add your provider key (OPENAI_API_KEY, ANTHROPIC_API_KEY or OPENROUTER_API_KEY), UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN and TOOL_APPROVAL_SECRET in Netlify dashboard → Site settings → Environment variables.
Cloudflare Workers
Cloudflare's current full-stack Next.js path is Workers through the OpenNext adapter. Wrangler 4.68+ can detect and configure an existing Next.js project automatically:
bunx wrangler deploy
Review the generated wrangler.jsonc and OpenNext files, run the generated preview script, then deploy. Add the live-chat variables (provider key, both Upstash values and TOOL_APPROVAL_SECRET) in Workers settings or through Wrangler secrets.
Cloudflare Pages is appropriate only if you convert the project to a static export and remove the live chat API.
Self-hosted (VPS / Docker)
bun run build
bun run start
# Runs on PORT=3000 by default
Set the live-chat variables (provider key, both Upstash values and TOOL_APPROVAL_SECRET) as shell env vars or through your orchestrator (PM2, systemd, Docker secrets, etc.).
Tell the rate limiter where the visitor IP comes from. Off Vercel, Netlify and Cloudflare Workers, the limiter uses the rightmost X-Forwarded-For entry, which is correct behind a single reverse proxy that appends to it (nginx proxy_add_x_forwarded_for, Caddy, most load balancers). For any other chain, set CHAT_CLIENT_IP_HEADER:
| Setup | CHAT_CLIENT_IP_HEADER |
|---|---|
| Cloudflare (proxy or Tunnel) in front of your server | cf-connecting-ip |
nginx that sets proxy_set_header X-Real-IP $remote_addr | x-real-ip |
| Single appending proxy (default) | leave unset |
Only name a header your own infrastructure overwrites. If the server is reachable without the proxy, a visitor can send that header themselves and get a fresh rate-limit window on every request.
Node VPS (PM2)
The build uses Next.js output: "standalone", so the server needs only the standalone bundle plus the static assets. ecosystem.example.cjs is a starting PM2 config:
bun run build
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public
pm2 start ecosystem.example.cjs
It binds to 127.0.0.1:3000, so put a reverse proxy (nginx, Caddy) in front of it. Rename the app, port or memory limit as you like. Keep the live-chat keys out of the file: export them in the shell that runs pm2 start, or load them from your secrets manager.
For Docker:
FROM oven/bun:1.3.5 AS build
WORKDIR /app
COPY package.json bun.lock bunfig.toml ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
Cost of a public agent
The chat endpoint is public, and every request can make several paid model calls. A rough ceiling for one request is maxSteps model calls (6 by default), each sending the whole conversation so far and generating up to ai.maxOutputTokens tokens (1500 by default). So spend grows as requests × maxSteps × conversation size: long conversations and many steps cost the most.
What bounds it:
- The rate limiter is per IP only. Upstash caps each client IP at 20 requests a minute. Someone rotating IPs is not capped as a whole, so the limiter alone is not a budget.
- Set a spend limit at the provider. Use a monthly budget / hard limit in your OpenAI or Anthropic console, or a credit limit on the OpenRouter key the site uses. This is the only cap that holds against rotating IPs.
- Keep
ai.allowedModelsto cheap models. The browser may request any model in that list; leave expensive models out of it. - Tune
ai.maxStepsandai.maxOutputTokensinsite.config.tsdown if your agent does not need them.
Custom domain checklist (whatever platform)
showcase: false: set it insite.config.tsand rebuild, so the kit's/components,/sections,/docsand/starterspages are not published on your product site.- Point DNS:
Arecord (orALIAS/CNAMEdepending on provider) to your deploy platform's IP or CNAME. - HTTPS: Most platforms issue Let's Encrypt certs automatically. Verify the green padlock.
product.urlinsite.config.ts: set it to your production domain (no trailing slash) soog:url, canonical links, the sitemap and/llms.txtmatch production.- Test the chat: open
your-domain.com, scroll to hero, send "Hello" — you should see a streamed response.
HSTS
next.config.ts sends Strict-Transport-Security: max-age=63072000, which is safe on any deploy. It deliberately omits includeSubDomains and preload: on a subdomain deploy they would force HTTPS on every sibling subdomain of your apex domain. Add includeSubDomains; preload to that header only if every subdomain of your apex serves HTTPS, and submit the domain to hstspreload.org yourself.
Troubleshooting
"OPENAI_API_KEY is not set" (or "ANTHROPIC_API_KEY" / "OPENROUTER_API_KEY is not set")
The chat API route returns 503 with <reason> Visitors see the scripted demo. if the key for the provider in site.config.ts (ai.provider, or AI_PROVIDER if set) is missing. The hero gracefully falls back to the scripted demo. To fix:
- Verify the key for that provider is in production (not just local).
- If you set
AI_PROVIDERorAI_MODEL, check they name a provider and a model listed insrc/lib/models.ts. - Redeploy after adding it.
Logs say "TOOL_APPROVAL_SECRET is not set"
The route warns in production when approval-gated tools exist and the secret is missing: tool approvals are then unsigned, so a client can forge one. Set TOOL_APPROVAL_SECRET to a long random string (openssl rand -hex 32), the same value on every server instance, and redeploy. The secret stops forged approvals, not replays of a genuine one; a tool with real side effects also needs server-side idempotency.
"rate limiting is not configured"
Production live chat requires UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN. Add both credentials and redeploy. The API intentionally returns 503 when Redis is missing or unavailable so model spend cannot become unbounded.
Every visitor hits "Too many requests" at once
The server found no client IP header, so all visitors share one rate-limit bucket (the server log says no client IP header found). Set CHAT_CLIENT_IP_HEADER for your proxy chain, as described under Self-hosted.
Fonts loading slowly
AgentKit self-hosts fonts via @fontsource, so there's no external network dependency. If you see slow font rendering:
- Check Network tab — fonts should load from same-origin with
fetchpriority="high". - Rebuild:
bun run build(Next.js may cache stale asset hashes).
Chat streaming cuts off
The chat route has a 30-second max duration (maxDuration = 30 in route.ts). If your agent takes longer than 30s, either:
- Raise
maxDuration(the route already runs onruntime = 'nodejs'; your platform's plan caps the limit). - Move the long-running inference to a separate worker.
I deleted a section and the page is broken
Check src/app/page.tsx — it imports each section. Remove the import AND the JSX usage together.
Questions? Reply to your purchase receipt.
— webdesignhot.com