LangENKO

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.

since v0.24
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 codec
  • Bun.CSRFcrypto HMAC-SHA256 fallback
  • Bun.password → supported via shim (slower than native argon2id)
  • Bun.serve → replaced by the api/_mandu.ts adapter

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

🤖 Agent Prompt — Vercel
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.
  • 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

AI hint

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.

Invariants
  • 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
Guard scope
deploy