{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "name": "Cloudflare for AI-Assisted Agencies",
  "version": "1.0.0",
  "description": "Machine-readable playbook for provisioning, deploying and verifying client-facing web work on Cloudflare using an AI coding agent. Written for agency operators; executable by agents.",
  "publisher": "Icarus Growth",
  "verified_against_docs": "2026-08",
  "human_readable": "index.html",
  "agent_readable": "llms.txt",

  "audience": {
    "primary": "Agency operators using AI coding agents (Claude Code, Cursor, Codex) to ship client work",
    "assumed_technical_level": "none to moderate",
    "agent_instruction": "If a human pointed you at this file, they want something deployed. Execute procedures.deploy end to end. Do not report success until verification.method has passed."
  },

  "prerequisites": {
    "required": [
      {
        "key": "CLOUDFLARE_API_TOKEN",
        "scope": "Account > Cloudflare Pages > Edit",
        "where": "dash.cloudflare.com -> profile icon -> Profile -> API Tokens -> Create Token -> Create Custom Token",
        "note": "Displayed once only. Cannot write zone DNS records."
      },
      {
        "key": "CLOUDFLARE_ACCOUNT_ID",
        "where": "dash.cloudflare.com -> Compute (Workers) -> Account ID in the right sidebar"
      }
    ],
    "required_for_custom_domains": [
      { "key": "CLOUDFLARE_GLOBAL_API_KEY", "note": "Distinct credential. The Pages token 401s on zone DNS endpoints." },
      { "key": "CLOUDFLARE_EMAIL" }
    ],
    "location": ".env at project root",
    "on_missing": "STOP. Tell the human exactly which value is missing and where to obtain it. Do not guess or proceed."
  },

  "procedures": {
    "deploy": {
      "summary": "Put a local folder live on a public URL.",
      "steps": [
        { "n": 1, "action": "Inspect folder; identify deploy dir (contains index.html, or the build output dir e.g. dist/). Run the build if one is required." },
        { "n": 2, "action": "Create the project if absent", "command": "npx wrangler pages project create <name> --production-branch main", "precheck": "npx wrangler pages project list" },
        { "n": 3, "action": "Deploy", "command": "npx wrangler pages deploy <dir> --project-name=<name> --branch=main", "critical": "ALWAYS pass --branch=main. Without it wrangler infers the branch from git and creates a PREVIEW deployment on a hashed URL, leaving the stable <name>.pages.dev alias unchanged." },
        { "n": 4, "action": "Verify", "ref": "verification" },
        { "n": 5, "action": "Report the live URL to the human." }
      ],
      "result_url": "https://<name>.pages.dev"
    },
    "attach_custom_domain": {
      "summary": "Two separate API calls with two different credentials. Doing only the first leaves the domain Pending forever with no error.",
      "steps": [
        { "n": 1, "method": "POST", "path": "/client/v4/accounts/<account>/pages/projects/<project>/domains", "auth": "Authorization: Bearer $CLOUDFLARE_API_TOKEN", "body": { "name": "app.yourdomain.com" } },
        { "n": 2, "method": "POST", "path": "/client/v4/zones/<zone>/dns_records", "auth": "X-Auth-Key: $CLOUDFLARE_GLOBAL_API_KEY + X-Auth-Email: $CLOUDFLARE_EMAIL", "body": { "type": "CNAME", "name": "app.yourdomain.com", "content": "<project>.pages.dev", "proxied": true } },
        { "n": 3, "action": "Poll https://cloudflare-dns.com/dns-query until the record resolves. Budget 10-15 minutes for certificate issuance after DNS lands." }
      ],
      "on_missing_global_key": "HARD FAIL. Do not warn and continue — a warning gets scrolled past and ships an NXDOMAIN to a client.",
      "preferred_alternative": "Use subdomains of one domain you already own. Unlimited, free, and Universal SSL covers one level of subdomain with no per-client certificate wait."
    }
  },

  "verification": {
    "rule": "A zero exit code from wrangler is NOT verification.",
    "method": [
      "Fetch the live URL.",
      "Assert HTTP 200.",
      "Assert the response body contains content traceable to the source files.",
      "Only then report success."
    ]
  },

  "failure_modes": [
    { "id": "full_snapshot_deploy", "severity": "high", "symptom": "Other pages in the project vanish after a deploy.", "cause": "A Pages deploy replaces the entire project; it does not merge with what is already there.", "fix": "Always deploy the whole intended tree, never a subfolder." },
    { "id": "false_success", "severity": "high", "symptom": "Agent reports success; page is broken or unchanged.", "cause": "Reporting the command's exit code rather than checking the result.", "fix": "See verification.method." },
    { "id": "domain_pending_forever", "severity": "high", "symptom": "Custom domain stuck in Pending; site unreachable; no error anywhere.", "cause": "Custom-domain attach does not create a DNS record, and the Pages token cannot write zone DNS.", "fix": "Run both calls in procedures.attach_custom_domain with their respective credentials." },
    { "id": "parking_wildcard_mask", "severity": "high", "symptom": "Subdomain returns 200 but shows a parking page or the wrong site.", "cause": "A catch-all/wildcard DNS record on the zone answers for unwired subdomains.", "fix": "Verify on page CONTENT, never on status code alone." },
    { "id": "nonatomic_propagation", "severity": "moderate", "symptom": "Post-deploy check fails, then passes seconds later.", "cause": "Static assets and the Functions/Worker bundle do not go live atomically.", "fix": "Give Worker-endpoint checks their own retry/backoff; never gate them on a static-asset check." },
    { "id": "stale_local_dns", "severity": "moderate", "symptom": "New DNS record reported as failing long after it landed.", "cause": "The local resolver cached the pre-record NXDOMAIN.", "fix": "Verify via https://cloudflare-dns.com/dns-query." },
    { "id": "transient_8000000", "severity": "low", "symptom": "Cloudflare error 8000000 on project create.", "cause": "Transient Cloudflare internal error.", "fix": "Retry only the create + deploy steps. Never re-run a full provisioning pipeline that also creates databases — it will duplicate them." }
  ],

  "build_types": {
    "static": { "platform": "Cloudflare Pages", "description": "Pre-written pages, identical for every visitor.", "good_for": ["landing pages", "lead magnets", "client updates", "reports", "documentation", "portfolios", "browser-side calculators"], "cost": "free, unlimited requests and bandwidth", "recommendation": "Start here. Covers roughly 80% of agency use cases." },
    "app": { "platform": "Cloudflare Pages Functions or a standalone Worker", "description": "Adds code that runs per request and can decide, store and fetch.", "good_for": ["logins", "sessions", "stored form submissions", "AI chat", "per-user content", "multi-client dashboards"], "cost": "free to 100,000 requests/day, then $5/month", "migration": "None. Add a functions/ directory to the same project — same deploy command, same domain." }
  },

  "routing": {
    "model": "file-based, no router config",
    "handlers": ["onRequest", "onRequestGet", "onRequestPost"],
    "context": ["request", "env", "data", "params", "next"],
    "examples": {
      "functions/api/health.ts": "GET /api/health",
      "functions/api/chat.ts": "POST /api/chat",
      "functions/api/auth/verify.ts": "/api/auth/verify",
      "functions/api/conversations/[id].ts": "/api/conversations/:id",
      "functions/api/_lib/": "leading underscore = never routed; shared code",
      "functions/api/_middleware.ts": "runs before every route in its directory and below"
    },
    "security_rule": "If one deployment serves multiple clients, resolve WHICH client the request is for before touching any token or database, and make an unknown Host a hard 404. Falling through to a default is a cross-client data leak, not a 404."
  },

  "limits_free_plan": {
    "pages_projects_per_account": { "value": 100, "note": "Not routinely increased. The real ceiling." },
    "builds_per_month": { "value": 500, "concurrent": 1 },
    "files_per_deployment": { "value": 20000, "max_file_size": "25 MiB" },
    "custom_domains_per_project": { "value": 100, "note": "Wildcards not supported on Pages" },
    "static_asset_requests": { "value": "unlimited", "note": "Free on all plans; never counts toward quota" },
    "worker_requests_per_day": { "value": 100000, "note": "Shared with Pages Functions. Resets midnight UTC. Error 1027 when exceeded." },
    "worker_cpu_ms_per_request": { "value": 10, "note": "I/O wait does not count" },
    "subrequests_per_request": { "value": 50, "internal_cloudflare": 1000 },
    "workers_per_account": { "value": 100 },
    "kv": { "reads_per_day": 100000, "writes_per_day": 1000, "deletes_per_day": 1000, "stored": "1 GB" },
    "zones": { "value": "unlimited", "note": "The abundant resource" }
  },

  "pricing_paid": {
    "workers_paid": { "minimum_monthly_usd": 5, "included_requests": 10000000, "included_cpu_ms": 30000000, "overage_per_million_requests_usd": 0.30, "overage_per_million_cpu_ms_usd": 0.02 },
    "domain": { "annual_usd_approx": "10-15", "note": "Cloudflare Registrar sells at cost" }
  },

  "scaling": {
    "ceiling": "100 Pages projects per account.",
    "trap": "A <name>.pages.dev hostname IS a Pages project. 'Save money by skipping domains' consumes the capped resource (projects) to save the uncapped one (zones are unlimited).",
    "principle": "Count what runs out before optimising what costs.",
    "fix": {
      "pattern": "One domain, one Worker on a wildcard route, a KV registry keyed by hostname.",
      "result": "Provisioning a new client becomes a single KV write — no project, no secret upload, no deploy, no certificate wait.",
      "config": { "routes": [{ "pattern": "*.yourdomain.com/*", "zone_name": "yourdomain.com" }], "assets": { "binding": "ASSETS", "not_found_handling": "single-page-application", "run_worker_first": true }, "kv_namespaces": [{ "binding": "TENANTS" }] },
      "why_worker_not_pages": "Pages rejects wildcard custom domains (error 8000015 'Domain is invalid'). Worker routes accept them, and Universal SSL already covers one level of subdomain.",
      "constraint": "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 already sent, with no error raised."
    }
  },

  "architecture_guidance": {
    "principle": "Cloudflare is the edge and the front door; heavy work belongs on compute you control.",
    "reason": "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. Waiting on a subrequest does not count against CPU time, so proxying out is cheap.",
    "layers": [
      { "layer": "Pages (static)", "runs": "built HTML/CSS/JS and assets", "why": "free and unlimited; never touches request quota" },
      { "layer": "Pages Functions", "runs": "auth, sessions, validation, form handling, proxying", "why": "no server, no cold-start config" },
      { "layer": "Workers KV", "runs": "config, tenant registries, feature flags", "why": "fast global reads; eventually consistent, so not for transactional data" },
      { "layer": "External DB", "runs": "relational data over HTTP (e.g. Neon serverless Postgres)", "why": "needs compatibility_flags = [\"nodejs_compat\"] to bundle" },
      { "layer": "External compute", "runs": "long jobs, LLM calls, batch processing", "why": "exceeds the Worker CPU budget" }
    ]
  },

  "commands": {
    "whoami": "npx wrangler whoami",
    "list_projects": "npx wrangler pages project list",
    "create_project": "npx wrangler pages project create <name> --production-branch main",
    "deploy": "npx wrangler pages deploy <dir> --project-name=<name> --branch=main",
    "local_dev": "npx wrangler pages dev <dir> --compatibility-flags=nodejs_compat",
    "set_secret": "npx wrangler pages secret put <NAME> --project-name=<name>",
    "deploy_worker": "npx wrangler deploy -c wrangler.toml"
  },

  "sources": [
    "https://developers.cloudflare.com/pages/platform/limits/",
    "https://developers.cloudflare.com/workers/platform/limits/",
    "https://developers.cloudflare.com/workers/platform/pricing/",
    "https://developers.cloudflare.com/pages/functions/pricing/"
  ]
}
