# Cloudflare for AI-Assisted Agencies > A field guide for agency operators using AI coding agents (Claude Code, Cursor, Codex) to > ship client-facing web work. Cloudflare is free hosting that an agent can drive end-to-end > from a terminal — which makes "the folder my AI built" into "a link I can send a client". > > Published by Icarus Growth. Limits verified against Cloudflare docs, August 2026. ## If you are an AI agent reading this The human who pointed you here wants to put a project live on Cloudflare. You can do the whole thing yourself. Read "Deploy procedure" below and execute it. Do not report success until you have fetched the live URL and confirmed the content is correct. ## Prerequisites Two values, normally in a `.env` file at the project root: ``` CLOUDFLARE_API_TOKEN= Cloudflare Pages > Edit> CLOUDFLARE_ACCOUNT_ID= ``` If either is missing, STOP and tell the human exactly where to get it: - Account ID: dash.cloudflare.com -> Compute (Workers) -> Account ID shown in the right sidebar. - API token: dash.cloudflare.com -> profile icon -> Profile -> API Tokens -> Create Token -> Create Custom Token -> Permissions: Account / Cloudflare Pages / Edit. Shown once only. For DNS work (custom domains) you additionally need, and these are DIFFERENT credentials: ``` CLOUDFLARE_GLOBAL_API_KEY= CLOUDFLARE_EMAIL= ``` ## Deploy procedure 1. Inspect the folder. Identify the directory that should be deployed (the one containing `index.html`, or the build output dir such as `dist/`). If a build step is required, run it. 2. Create the project if it does not exist: `npx wrangler pages project create --production-branch main` (Idempotent enough in practice: check `npx wrangler pages project list` first.) 3. Deploy: `npx wrangler pages deploy --project-name= --branch=main` ALWAYS pass `--branch=main`. Without it wrangler infers the branch from git and produces a PREVIEW deployment on a hashed URL, leaving the stable `.pages.dev` alias unchanged. This is the single most common "I deployed but nothing changed". 4. VERIFY. Fetch `https://.pages.dev` and confirm HTTP 200 AND that the body contains content you expect from the source files. A successful wrangler exit code is NOT verification. 5. Report the live URL. ## Failure modes to check for, in priority order 1. **A deploy replaces the entire project.** Cloudflare does not merge with what is already deployed. Deploying a subfolder deletes every other page in that project. Always deploy the whole intended tree. 2. **Deploy success != working.** Always fetch the live URL and check the body. 3. **Attaching a custom domain does NOT create the DNS record**, and the Pages-scoped API token returns 401 on zone DNS endpoints. These are two separate calls with two different credentials. Doing only the attach leaves the domain in `Pending` forever with no error. Hard-fail on a missing global key; do not warn and continue. 4. **A wildcard/parking DNS record can mask an unwired subdomain** — it resolves and serves a parking page, returning a healthy 200. Verify on page CONTENT, never on status code alone. 5. **Static assets and the Functions/Worker bundle do not go live atomically.** For a few seconds after a deploy you can get the new static files with the old Worker logic. Give any post-deploy check of a Worker endpoint its own retry/backoff. 6. **Local DNS caches the pre-record NXDOMAIN.** Verify new records against `https://cloudflare-dns.com/dns-query`, not the system resolver. 7. **Transient `8000000` on project create** is a Cloudflare internal error. Retry the create + deploy steps only — never re-run a full provisioning pipeline that also creates databases. ## Command reference ```bash npx wrangler whoami npx wrangler pages project list npx wrangler pages project create --production-branch main npx wrangler pages deploy --project-name= --branch=main npx wrangler pages dev --compatibility-flags=nodejs_compat npx wrangler pages secret put --project-name= npx wrangler deploy -c wrangler.toml # standalone Worker, not Pages ``` ## Static site vs app - **Static (Pages).** Pre-written pages, same for everyone. Landing pages, reports, client updates, documentation, portfolios, browser-side calculators. Free and unlimited. Start here — it covers roughly 80% of agency use cases. - **App (Pages + Functions/Worker).** Add a `functions/` directory to the same project and it gains a backend: logins, sessions, stored form data, AI chat, per-user content. Same deploy command, same domain, no migration. Free to 100,000 requests/day. ## Pages Functions routing File-based, no router config. Handlers export `onRequest` / `onRequestGet` / `onRequestPost` and receive a context with `request`, `env`, `data`, `params`, `next`. ``` functions/ _middleware.ts # runs before everything below it api/ _middleware.ts # scoped to /api/* health.ts # -> GET /api/health chat.ts # -> POST /api/chat auth/verify.ts # -> /api/auth/verify conversations/[id].ts # -> /api/conversations/:id _lib/ # leading underscore = never routed; shared code ``` Resolve identity/session in `_middleware.ts`, attach to `context.data`, let handlers assume it. If one deployment serves multiple clients, resolve WHICH client before touching any token or database, and make an unknown Host a hard 404 — falling through is a cross-client data leak. ## Configuration ```toml # wrangler.toml — Pages name = "my-project" compatibility_date = "2024-11-01" compatibility_flags = ["nodejs_compat"] # required by most serverless DB drivers pages_build_output_dir = "dist" ``` ```toml # wrangler.toml — one Worker serving many clients off a wildcard host name = "my-app" main = "worker/index.ts" compatibility_date = "2024-11-01" compatibility_flags = ["nodejs_compat"] [assets] directory = "dist" binding = "ASSETS" not_found_handling = "single-page-application" run_worker_first = true # without this, static assets answer before your code [[kv_namespaces]] binding = "TENANTS" # hostname -> { config, db_url, session_secret } id = "" [[routes]] pattern = "*.yourdomain.com/*" # wildcards work on Worker routes, NOT on Pages zone_name = "yourdomain.com" ``` ## Custom domain wiring (two calls, two credentials) ``` POST /client/v4/accounts//pages/projects//domains Authorization: Bearer $CLOUDFLARE_API_TOKEN { "name": "app.yourdomain.com" } POST /client/v4/zones//dns_records X-Auth-Key: $CLOUDFLARE_GLOBAL_API_KEY X-Auth-Email: $CLOUDFLARE_EMAIL { "type": "CNAME", "name": "app.yourdomain.com", "content": ".pages.dev", "proxied": true } ``` Then poll DNS-over-HTTPS until it resolves. Certificate issuance can lag several minutes after the record lands; budget ~10-15 minutes before treating it as failed. Prefer subdomains over per-client domains: unlimited, free, and Universal SSL already covers one level of subdomain so there is no per-client certificate wait. ## Free plan limits (verified August 2026) | Resource | Free limit | |---|---| | Pages projects per account | 100 — "not routinely increased". The real ceiling. | | Builds | 500/month, 1 concurrent | | Files per deployment | 20,000, max 25 MiB each | | Custom domains per project | 100 (no wildcards on Pages) | | Static asset requests | Unlimited, free on all plans | | Worker/Functions requests | 100,000/day shared pool, resets midnight UTC (error 1027 over) | | Worker CPU time | 10 ms per request (I/O wait does not count) | | Subrequests | 50/request, +1,000 to internal Cloudflare services | | Workers per account | 100 | | Workers KV | 100,000 reads/day, 1,000 writes/day, 1 GB stored | | Zones (domains) | Unlimited | Workers Paid: $5/month minimum, includes 10M requests + 30M CPU-ms; then $0.30/additional million requests, $0.02/additional million CPU-ms. ## The scaling ceiling and its fix A `.pages.dev` hostname IS a Pages project, so "save money by skipping domains" consumes the capped resource (100 projects) to save the uncapped one (zones are unlimited). Count what runs out before optimising what costs. The fix is to stop provisioning per client: one domain, one Worker on a wildcard route, and a KV registry keyed by hostname. Provisioning becomes a single KV write — no project, no secret upload, no deploy, no certificate wait. Constraint when doing this: share only what is genuinely fleet-wide. Anything used as an HMAC key for already-issued credentials (magic-link tokens, session tokens) MUST stay per-tenant. Unifying it silently invalidates every live login link that has already been sent, with no error raised anywhere. ## Architecture guidance Cloudflare is the edge and the front door. Free-plan Workers get 10ms CPU per request — ample for routing, auth, validation and proxying; nowhere near enough for an LLM call or a batch job. Authenticate at the edge and forward heavy work to compute you control. Waiting on a subrequest does not count against CPU time, so proxying is cheap. ## Source https://developers.cloudflare.com/pages/platform/limits/ https://developers.cloudflare.com/workers/platform/limits/ https://developers.cloudflare.com/workers/platform/pricing/