Cloudflare Pages
`mandu deploy --target=cf-pages` emits a `wrangler.toml` + a `functions/_middleware.ts` glue file. Artifact-only in Phase 13 — runtime compatibility via the Phase 15 Workers adapter.
On this page
Cloudflare Pages
mandu deploy --target=cf-pages emits a wrangler.toml and a Pages
Functions middleware at functions/_middleware.ts. With --execute,
Mandu drives wrangler pages deploy using a token stored in
Bun.secrets.
Phase 13 is artifact-only for runtime compatibility. Full CookieMap / CSRF / password polyfills ship through
@mandujs/edgein Phase 15 — see the edge adapter. The Pages Functions path is production-ready for small/static-heavy apps today.
# Emit artifacts only
mandu deploy --target=cf-pages
# Set up the API token
mandu deploy --target=cf-pages --set-secret CLOUDFLARE_API_TOKEN=cf_...
# Full deploy
mandu deploy --target=cf-pages --execute
Prerequisites
# Install wrangler
bun add -g wrangler
# Log in (once)
wrangler login
Minimum version: wrangler 3.0.0. Older versions exit with CLI_E206.
Emitted files
.
├── wrangler.toml
└── functions/
└── _middleware.ts # catch-all Pages Functions middleware
Pages + Functions vs standalone Workers
| Path | Use when | Adapter |
|---|---|---|
| Pages + Functions (this page) | You want static assets + serverless functions tied to a git repo, tied together under a single Pages project. | mandu deploy --target=cf-pages |
| Standalone Worker | You want a dedicated Worker (mandu build --target=workers) with separate deployment + fine-grained route control. |
@mandujs/edge/workers |
Both target Cloudflare's V8 isolate runtime; the shipping model is different. For most Mandu apps, Workers is the better fit — Pages Functions are best for mostly-static sites with a sprinkle of dynamic logic.
The wrangler.toml
# wrangler.toml
name = "my-mandu-app"
compatibility_date = "2025-01-01"
compatibility_flags = ["nodejs_als"]
pages_build_output_dir = "public"
[env.production]
name = "my-mandu-app"
# Bindings go here. Example: a KV namespace for session storage.
# [[env.production.kv_namespaces]]
# binding = "SESSIONS"
# id = "xxxxxxxxxxxxxxxx"
compatibility_flags = ["nodejs_als"] enables AsyncLocalStorage so
Mandu's binding accessors (getWorkersEnv, getWorkersCtx) work
across waitUntil callbacks. Without the flag, bindings are only
accessible during the request — fetch-time only.
The middleware — functions/_middleware.ts
// functions/_middleware.ts
import manifest from "../.mandu/routes.manifest.json";
import "../.mandu/cf-pages/register.js";
import { createWorkersHandler } from "@mandujs/edge/workers";
const handler = createWorkersHandler(manifest, {
cssPath: "/.mandu/client/globals.css",
});
export const onRequest: PagesFunction = async (context) => {
// Static assets in /public take precedence via Pages routing.
// Every other path falls through to the Workers handler.
return handler(context.request, context.env, context.ctx);
};
Required secrets
| Name | Purpose | Store |
|---|---|---|
CLOUDFLARE_API_TOKEN |
wrangler auth for non-interactive deploys | Bun.secrets → OS keychain |
App-runtime secrets are set via wrangler pages secret put, NOT
mandu deploy --set-secret:
wrangler pages secret put DATABASE_URL --project-name my-mandu-app
wrangler pages secret put SESSION_SECRET --project-name my-mandu-app
Secrets live in Cloudflare's control plane, encrypted at rest,
injected into env at runtime.
Bindings — KV, R2, D1
Declare bindings in wrangler.toml and access them via the accessor
helpers:
# wrangler.toml
[[env.production.kv_namespaces]]
binding = "SESSIONS"
id = "xxxxxxxxxxxxxxxx"
[[env.production.r2_buckets]]
binding = "UPLOADS"
bucket_name = "my-app-uploads"
[[env.production.d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "yyyyyyyyyyyyyyyy"
Inside your handlers:
import { getWorkersEnv } from "@mandujs/edge/workers";
export async function GET() {
const env = getWorkersEnv();
const session = await env!.SESSIONS.get("sid_123");
const avatar = await env!.UPLOADS.get("u/alice.png");
const row = await env!.DB.prepare("SELECT * FROM users WHERE id = ?").bind(1).first();
// ...
}
Runtime limits
| Limit | Free | Paid |
|---|---|---|
| CPU time per invocation | 10 ms | 30 s |
| Memory | 128 MB | 128 MB |
| Request body size | 100 MB | 500 MB |
| Script size (gzip) | 1 MB | 10 MB |
The free-tier 10ms cap is tight — a simple SSR page with one DB fetch will usually fit, but heavy contracts or large islands may push you over. Consider the standalone Workers adapter for more headroom.
Cold start
V8 isolates cold-start in ~1–5ms — faster than any Node-based runtime. Per-request overhead:
- Cold: ~5ms isolate boot + handler init
- Warm: ~1ms dispatch + your own work
Common errors
CLI_E213: Edge-runtime compatibility warning — an imported
module uses fs, child_process, or a native binding that won't run
in V8 isolates. Check the log for the specific import; swap for a
Web-standard equivalent.
CLI_E207: required secret CLOUDFLARE_API_TOKEN is not present —
run mandu deploy --target=cf-pages --set-secret CLOUDFLARE_API_TOKEN=cf_....
Deploy succeeds but 500 on every route — check wrangler logs:
wrangler pages deployment tail. Usually a missing
compatibility_flags = ["nodejs_als"] or a missing runtime env var.
Bindings return undefined — confirm
compatibility_flags = ["nodejs_als"] is present. Without it,
getWorkersEnv() falls back to a per-request WeakMap that doesn't
survive waitUntil callbacks.
🤖 Agent Prompt
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/deploy/cf-pages to my project.
Summary of the page:
cf-pages is the Pages+Functions variant (as distinct from `@mandujs/edge/workers` which is the dedicated Workers adapter). Emits wrangler.toml + functions/_middleware.ts routing every request to the Worker handler. Runtime compat via Phase 15's Bun.CookieMap → LegacyCookieCodec polyfills.
Required invariants — must hold after your changes:
- Distinct from `@mandujs/edge/workers` — cf-pages uses Pages Functions (per-project), Workers adapter uses standalone Worker
- Required secret: `CLOUDFLARE_API_TOKEN` — stored in Bun.secrets
- Minimum wrangler version: 3.0.0
- App bindings (KV, R2, D1) are declared in wrangler.toml — secret values go via `wrangler pages secret put`
- Runtime compatibility lands in Phase 15 via @mandujs/edge — some Bun APIs currently shim slowly
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
- Edge — Cloudflare Workers — standalone Workers adapter (recommended over Pages Functions for most Mandu apps).
- Edge — Polyfill mapping — Bun.CookieMap → legacy codec, etc.
- Deploy index — full adapter matrix.
For Agents
{
"schema": "mandu.deploy.cf-pages/v0.24",
"command": "mandu deploy --target=cf-pages",
"artifacts": ["wrangler.toml", "functions/_middleware.ts"],
"provider_cli": { "binary": "wrangler", "min_version": "3.0.0" },
"runtime": "cloudflare-v8-isolate (Pages Functions)",
"required_compat_flags": ["nodejs_als"],
"vs_standalone_worker": "`@mandujs/edge/workers` → `mandu build --target=workers` is the preferred path for most apps",
"secrets_mandu_side": ["CLOUDFLARE_API_TOKEN"],
"secrets_app_side": "via `wrangler pages secret put`, NOT `--set-secret`",
"rules": [
"Always include `compatibility_flags = ['nodejs_als']` — without it, bindings don't survive `waitUntil`",
"App-runtime secrets go via `wrangler pages secret put`",
"For more CPU headroom than 10ms free tier, use standalone Workers (`@mandujs/edge/workers`)"
]
}For Agents
cf-pages is the Pages+Functions variant (as distinct from `@mandujs/edge/workers` which is the dedicated Workers adapter). Emits wrangler.toml + functions/_middleware.ts routing every request to the Worker handler. Runtime compat via Phase 15's Bun.CookieMap → LegacyCookieCodec polyfills.
- Distinct from `@mandujs/edge/workers` — cf-pages uses Pages Functions (per-project), Workers adapter uses standalone Worker
- Required secret: `CLOUDFLARE_API_TOKEN` — stored in Bun.secrets
- Minimum wrangler version: 3.0.0
- App bindings (KV, R2, D1) are declared in wrangler.toml — secret values go via `wrangler pages secret put`
- Runtime compatibility lands in Phase 15 via @mandujs/edge — some Bun APIs currently shim slowly