Vercel
`mandu deploy --target=vercel` emits a `vercel.json` + a Node SSR function at `api/_mandu.ts`. With `--execute` and `vercel ≥ 28.0.0` installed, Mandu triggers a deploy.
On this page
Vercel
mandu deploy --target=vercel emits a vercel.json and a single
Node SSR function at api/_mandu.ts. With --execute, Mandu invokes
the vercel CLI to push a deploy — using a token stored in
Bun.secrets so it never appears on argv.
# Emit artifacts only
mandu deploy --target=vercel
# Set up the API token
mandu deploy --target=vercel --set-secret VERCEL_TOKEN=vl_...
# First-time link
vercel link
# Full deploy (requires vercel CLI + token)
mandu deploy --target=vercel --execute
Prerequisites
# Install Vercel CLI globally via Bun
bun add -g vercel
# Log in (once) — stores a token in ~/.local/share/vercel
vercel login
Minimum version: vercel 28.0.0. Older versions exit with CLI_E206.
Emitted files
.
├── vercel.json
├── api/
│ └── _mandu.ts # Node SSR entry (Vercel Functions)
└── public/ # static assets (no adapter touch)
The vercel.json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": null,
"buildCommand": "bun run build",
"installCommand": "bun install --frozen-lockfile",
"outputDirectory": "public",
"functions": {
"api/_mandu.ts": {
"runtime": "nodejs20.x",
"memory": 1024,
"maxDuration": 60
}
},
"rewrites": [
{ "source": "/(.*)", "destination": "/api/_mandu" }
]
}
The SSR entry — api/_mandu.ts
Mandu emits a minimal adapter that bridges Vercel's Node runtime into a Fetch-compatible request:
// api/_mandu.ts
import type { VercelRequest, VercelResponse } from "@vercel/node";
import manifest from "../.mandu/routes.manifest.json";
import "../.mandu/vercel/register.js";
import { createNodeHandler } from "@mandujs/core/adapters/node";
const handler = createNodeHandler(manifest, {
cssPath: "/.mandu/client/globals.css",
});
export default async function (req: VercelRequest, res: VercelResponse) {
return handler(req, res);
}
This file is auto-regenerated on every mandu deploy --target=vercel.
Hand edits are overwritten — fork the adapter if you need a custom
shape.
Runtime: Node.js 20
Phase 13.1's Vercel adapter runs on Node.js 20. Vercel's Bun runtime is not yet GA on Functions, so Mandu's Bun-native APIs are bridged via:
Bun.CookieMap→ a Node-compatible cookie codecBun.CSRF→cryptoHMAC-SHA256 fallbackBun.password→ supported via shim (slower than native argon2id)Bun.serve→ replaced by theapi/_mandu.tsadapter
If your app uses Bun.sql against Postgres, set the DATABASE_URL
env var at deploy time and ensure the driver works on Node (most do).
For Bun.s3 hit against S3/R2 use aws4fetch or any Node S3 SDK.
For edge runtime (Cloudflare Workers) see the edge adapter — distinct from Vercel Functions.
Required secrets
| Name | Purpose | Store |
|---|---|---|
VERCEL_TOKEN |
vercel CLI auth for --execute |
Bun.secrets → OS keychain |
App-runtime env vars go through vercel env add, NOT --set-secret:
vercel env add DATABASE_URL production
vercel env add SESSION_SECRET production
vercel env add JWT_SECRET production
vercel env add stores encrypted values in Vercel's control plane and
injects them at build/runtime.
Project linking
Before the first --execute, link the project to Vercel:
# Links this directory to a Vercel project
vercel link
# Pulls the linked project's env vars into .vercel/.env.*
vercel env pull
The .vercel/ directory holds the project binding — gitignore it.
Cold start
Typical cold start for a small Mandu app:
- First request after deploy: ~900–1200ms (Node boot + handler init)
- Warm request: ~50–150ms (SSR render + DB roundtrip)
Keep-alive is provided by Vercel's platform — no tuning required.
Common errors
CLI_E205: required provider CLI vercel is missing — install
with bun add -g vercel.
CLI_E207: required secret VERCEL_TOKEN is not present — run
mandu deploy --target=vercel --set-secret VERCEL_TOKEN=vl_....
Deploy succeeds but 500 on SSR — check vercel logs <deployment>.
Usually a missing runtime env var (DATABASE_URL, SESSION_SECRET).
"Module not found: .mandu/routes.manifest.json" — run
mandu build before mandu deploy --target=vercel. The manifest is
a build output.
🤖 Agent Prompt
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/vercel to my project.
Summary of the page:
Vercel adapter: emits vercel.json with a rewrite to the Node SSR function at api/_mandu.ts. Runtime: Node 20 (Vercel's Bun support is not GA yet in Phase 13.1). Static routes served from `public/`; dynamic routes through the SSR function. `VERCEL_TOKEN` is the Mandu-side secret.
Required invariants — must hold after your changes:
- Runtime is Node.js 20 — Bun-specific APIs that are Node-incompatible will fail at runtime (see edge adapter for Workers path)
- Static routes served from `public/`; every dynamic request rewrites to `/api/_mandu`
- Required secret: `VERCEL_TOKEN` — stored in Bun.secrets
- Minimum vercel CLI version: 28.0.0
- Environment variables must be set via `vercel env add` — the emitted vercel.json never contains secret values
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Related
- Deploy index — full adapter matrix.
- Edge adapter — Cloudflare Workers for edge runtime; Vercel Edge adapter coming in Phase 15.2.
For Agents
{
"schema": "mandu.deploy.vercel/v0.24",
"command": "mandu deploy --target=vercel",
"artifacts": ["vercel.json", "api/_mandu.ts"],
"provider_cli": { "binary": "vercel", "min_version": "28.0.0" },
"runtime": "nodejs20.x",
"memory_mb_default": 1024,
"max_duration_s_default": 60,
"secrets_mandu_side": ["VERCEL_TOKEN"],
"secrets_app_side": "via `vercel env add`, NOT `--set-secret`",
"rules": [
"Run `vercel link` once before first `--execute`",
"App-runtime env vars go via `vercel env add`",
"For edge runtime use `@mandujs/edge` + Cloudflare Workers, not Vercel Functions"
]
}For Agents
Vercel adapter: emits vercel.json with a rewrite to the Node SSR function at api/_mandu.ts. Runtime: Node 20 (Vercel's Bun support is not GA yet in Phase 13.1). Static routes served from `public/`; dynamic routes through the SSR function. `VERCEL_TOKEN` is the Mandu-side secret.
- Runtime is Node.js 20 — Bun-specific APIs that are Node-incompatible will fail at runtime (see edge adapter for Workers path)
- Static routes served from `public/`; every dynamic request rewrites to `/api/_mandu`
- Required secret: `VERCEL_TOKEN` — stored in Bun.secrets
- Minimum vercel CLI version: 28.0.0
- Environment variables must be set via `vercel env add` — the emitted vercel.json never contains secret values