LangENKO

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.

since v0.24
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/edge in 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

🤖 Agent Prompt — Cloudflare Pages
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.

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

AI hint

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.

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