← Back to dashboard
cloudflarehostingfresh

Cloudflare + Claude Code Integration Guide

What is an edge platform?

The real model

Code at the nearest PoP, behind one anycast IP.

"The edge" means your code executes in the data centre closest to each request instead of a fixed origin region. Cloudflare runs every service on every server in every PoP behind one anycast IP, so there is no region to choose — the network routes to the nearest node (hundreds of cities, 405+ Tbps, ~50 ms to 95% of users). Workers boot in ~0 ms via V8 isolates rather than containers. The trade-offs to design around: no persistent TCP (reach for HTTP stores — R2 / KV / D1), KV is eventually consistent, and when you would rather run near your database than near the user, use Smart Placement.

The four building blocks

Workers run the code; D1, KV and R2 hold the data. Tap a card to dig in.

Why R2 has zero egress fees

Egressis data leaving the provider out to the internet — every file your users download. Traditional clouds bill it per gigabyte on top of storage, so at scale the bandwidth bill dwarfs storage and quietly locks your data in (moving or serving it gets expensive — known as “data gravity”). Cloudflare charges $0 for egressbecause it already runs a global network with huge peering — bandwidth isn’t a metered resale cost for them, and dropping egress fees is a deliberate move to break that lock-in.

Egress cost playgroundserving 1,000 GB / month
AWS S3 egress (~$0.09/GB)$90/mo
Cloudflare R2 egress$0/mo

Same bytes, same users. On R2 you still pay for storage and operations — but moving the data out to the internet is $90/mo cheaper here, and the gap only widens with traffic. (S3 figure is the first-tier rate; it tiers down at scale — R2 stays $0.)

It’s exactly why this dashboard serves its tech thumbnails from R2 (thumbs.codeamanilabs.org) — shipping images to a data-conscious East-African audience costs nothing to egress.

Text
 ██████╗██╗      ██████╗ ██╗   ██╗██████╗ ███████╗██╗      █████╗ ██████╗ ███████╗
██╔════╝██║     ██╔═══██╗██║   ██║██╔══██╗██╔════╝██║     ██╔══██╗██╔══██╗██╔════╝
██║     ██║     ██║   ██║██║   ██║██║  ██║█████╗  ██║     ███████║██████╔╝█████╗
██║     ██║     ██║   ██║██║   ██║██║  ██║██╔══╝  ██║     ██╔══██║██╔══██╗██╔══╝
╚██████╗███████╗╚██████╔╝╚██████╔╝██████╔╝██║     ███████╗██║  ██║██║  ██║███████╗
 ╚═════╝╚══════╝ ╚═════╝  ╚═════╝ ╚═════╝ ╚═╝     ╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝

Cloudflare + Claude Code Integration Guide

Focus: Building, deploying, and managing Cloudflare Workers, D1, KV, R2, and the full Cloudflare platform from Claude Code using the official MCP server and Wrangler CLI.

Overview

Cloudflare's developer platform offers Workers (serverless), D1 (SQLite at the edge), KV (key-value), R2 (object storage), Pages, and 2,500+ API endpoints. Claude Code integrates natively through the official cloudflare/mcp-server-cloudflare MCP server and wrangler CLI. Work from the root of your Workers project — Claude Code reads wrangler.jsonc to understand your bindings automatically.

Here is the big picture — a single request hits the edge, runs your Worker, and reaches whichever bindings it needs:

Official Documentation


MCP Server Setup

Official Cloudflare MCP Server

The official server is at github.com/cloudflare/mcp-server-cloudflare. It uses "Code Mode" — Claude writes JavaScript to call any of 2,500+ Cloudflare API endpoints via two unified tools: search() and execute().

Bash
# Add via Claude Code CLI (npx transport)
claude mcp add cloudflare -- npx -y @cloudflare/mcp-server-cloudflare

.mcp.json Configuration

JSON
{
  "mcpServers": {
    "cloudflare": {
      "command": "npx",
      "args": ["-y", "@cloudflare/mcp-server-cloudflare"],
      "env": {
        "CLOUDFLARE_API_TOKEN": "${CLOUDFLARE_API_TOKEN}",
        "CLOUDFLARE_ACCOUNT_ID": "${CLOUDFLARE_ACCOUNT_ID}"
      }
    }
  }
}

Available MCP Tools

ToolDescription
searchSearch for any Cloudflare API endpoint by description
executeExecute any Cloudflare API operation by endpoint ID
workers_listList all Workers scripts
workers_get_workerGet Worker script code
workers_get_worker_codeFetch Worker source
d1_database_queryRun SQL against a D1 database
d1_databases_listList D1 databases
kv_namespaces_listList KV namespaces
kv_namespace_getRead KV values
r2_buckets_listList R2 buckets
accounts_listList Cloudflare accounts

CLI Integration (Wrangler)

Installation

Bash
npm install -g wrangler

Authentication

Bash
# Interactive OAuth login
wrangler login

# Use API token (for CI)
export CLOUDFLARE_API_TOKEN=...

Key Commands

Bash
# Create a new Worker project
npm create cloudflare@latest my-worker -- --type worker

# Local development (with hot reload)
wrangler dev

# Deploy to Cloudflare
wrangler deploy

# View production logs (live tail)
wrangler tail my-worker

# D1 database commands
wrangler d1 create my-database
wrangler d1 execute my-database --command "CREATE TABLE users (id INTEGER PRIMARY KEY)"
wrangler d1 execute my-database --file schema.sql
wrangler d1 migrations apply my-database --local
wrangler d1 migrations apply my-database --remote

# KV namespace commands
wrangler kv:namespace create MY_NAMESPACE
wrangler kv:key put --binding MY_NAMESPACE "key" "value"
wrangler kv:key get --binding MY_NAMESPACE "key"

# R2 bucket commands
wrangler r2 bucket create my-bucket
wrangler r2 object put my-bucket/path/to/file.json --file ./data.json

# Pages deployment
wrangler pages deploy dist/ --project-name my-site

# Secret management
wrangler secret put MY_SECRET
wrangler secret list

Worker Example

src/index.ts:

TypeScript
export interface Env {
  DB: D1Database;
  KV: KVNamespace;
  MY_SECRET: string;
}

export default {
  async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(req.url);

    if (url.pathname === "/users") {
      const { results } = await env.DB.prepare(
        "SELECT * FROM users ORDER BY created_at DESC LIMIT 10"
      ).all();
      return Response.json(results);
    }

    if (url.pathname === "/kv") {
      const value = await env.KV.get("my-key");
      return new Response(value ?? "not found");
    }

    return new Response("Not Found", { status: 404 });
  },
};

wrangler.jsonc:

JSON
{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2025-01-01",
  "d1_databases": [
    { "binding": "DB", "database_name": "my-database", "database_id": "..." }
  ],
  "kv_namespaces": [
    { "binding": "KV", "id": "..." }
  ]
}

Pages & Functions

Verified against Cloudflare's official docs (developers.cloudflare.com/pages/functions). Pages serves your static build; Pages Functions add server-side code on the same deploy — file-based routing out of a functions/ directory, running on Workers.

Pages is two layers in one deploy: static assets plus an optional functions/ directory that Cloudflare compiles into a single Worker. Files map to URL paths automatically:

File-based routing

A file's path under functions/ becomes its route:

FileRoute
functions/index.ts/
functions/api/hello.ts/api/hello
functions/users/[user].ts/users/:user (single segment → params.user string)
functions/api/[[path]].ts/api/* (catch-all → params.path array)

More specific routes (fewer wildcards) win over catch-alls.

Pages Function example

A catch-all API handler at functions/api/[[path]].ts. Each onRequest (or method-specific onRequestGet / onRequestPost) receives an EventContext with request, env, params, waitUntil, next, and data. The PagesFunction<Env> generic types your bindings:

TypeScript
interface Env {
  KV: KVNamespace;
  DB: D1Database;
}

// Handles GET /api/anything/here
export const onRequestGet: PagesFunction<Env> = async (context) => {
  const { params, env } = context;
  // params.path is the segments after /api/ as a string[]
  const segments = params.path as string[];

  if (segments[0] === "ping") {
    return Response.json({ ok: true, ts: Date.now() });
  }

  const cached = await env.KV.get(segments.join("/"));
  return cached
    ? new Response(cached)
    : new Response("Not Found", { status: 404 });
};

// A bare onRequest runs for any verb without a more specific onRequestVerb export.
export const onRequest: PagesFunction<Env> = async ({ next }) => {
  return next(); // fall through to the static asset server
};

Deploy

Bash
# Build your site, then deploy the output directory (Functions in ./functions are bundled)
wrangler pages deploy dist/ --project-name my-site

# Local dev with Functions + bindings emulated
wrangler pages dev dist/

Bindings

Pages Functions read bindings off context.env, same as Workers. Configure them in wrangler.jsonc (or the Pages project's dashboard Settings → Bindings for production/preview). Keep compatibility_date current.

JSON
{
  "name": "my-site",
  "pages_build_output_dir": "dist",
  "compatibility_date": "2025-01-01",
  "kv_namespaces": [
    { "binding": "KV", "id": "..." }
  ],
  "d1_databases": [
    { "binding": "DB", "database_name": "my-database", "database_id": "..." }
  ]
}

_routes.json

Cloudflare auto-generates this, but you can ship your own at the build-output root to control which paths invoke Functions (vs. serving a static asset directly). exclude takes priority over include; wildcards match any number of segments:

JSON
{
  "version": 1,
  "include": ["/api/*"],
  "exclude": ["/api/static/*"]
}

Gotcha: if a path matches no include rule (or hits an exclude), the request is served as a static asset and your Function never runs — a silent 404/wrong-content instead of an error. When an API route mysteriously bypasses your handler, check _routes.json first. Run wrangler pages deploy to regenerate the auto version.


Environment Variables

Bash
# Required
CLOUDFLARE_API_TOKEN=...         # From dash.cloudflare.com → Profile → API Tokens
CLOUDFLARE_ACCOUNT_ID=...        # From dash.cloudflare.com (right sidebar)

# Wrangler picks these up automatically from environment
# Or use: wrangler secret put MY_SECRET for runtime secrets

Automation Workflows

Claude Code Hook: Auto-deploy on Save

.claude/settings.json:

JSON
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "if echo \"$CLAUDE_FILE_PATH\" | grep -q 'src/'; then echo 'Worker source changed — run wrangler deploy to push'; fi"
          }
        ]
      }
    ]
  }
}

Slash Command: Deploy and Tail Logs

.claude/commands/cf-deploy.md:

Markdown
Deploy the current Cloudflare Worker and confirm it's live.

1. Use Bash to run `wrangler deploy` and capture the deployed URL
2. Use Bash to run `wrangler tail --format pretty` for 10 seconds to check for errors
3. Report the deployed Worker URL and any runtime errors observed

Usage: /project:cf-deploy

GitHub Actions: CI Deploy to Cloudflare Workers

YAML
# .github/workflows/cloudflare.yml
name: Deploy Worker
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '22' }
      - run: npm ci
      - name: Run D1 migrations
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
        run: npx wrangler d1 migrations apply my-database --remote
      - name: Deploy Worker
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          accountId: ${{ secrets.CF_ACCOUNT_ID }}

R2: Create Buckets & API Tokens (step-by-step)

Verified against Cloudflare's official docs (developers.cloudflare.com/r2). Bucket names: lowercase letters, numbers, hyphens only.

You have three clean paths into R2 — pick the one that matches the job, and serving objects publicly is just as straightforward:

Create a bucket

Dashboardopen R2 → Overview

  1. Go to R2 object storageOverview.
  2. Select Create bucket.
  3. Enter a name, pick a location + default storage class.
  4. Select Create bucket.

Wrangler (auth via wrangler login, no keys needed):

Bash
npx wrangler r2 bucket create my-bucket
npx wrangler r2 bucket list

REST API (needs an API token with R2 edit — see below):

Bash
curl https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/r2/buckets \
  -H "Authorization: Bearer $R2_ADMIN_TOKEN" -H "Content-Type: application/json" \
  --data '{"name":"my-bucket"}'

Get R2 API tokens (S3 Access Key ID + Secret)

Needed for S3 SDKs (boto3, AWS SDK, rclone) and for an app to read/write objects. Wrangler does not need these.

Dashboardopen R2 API tokens

  1. R2 → Overview → under Account details, select Manage next to API Tokens.
  2. Choose Create Account API token (tied to the account, survives user removal — best for automation) or Create User API token (tied to your user).
  3. Under Permissions pick one: Object Read & Write (typical), Object Read, Admin Read & Write, or Admin Read.
  4. Select Apply to specific buckets only and choose your bucket (least privilege).
  5. Create API Token, then copy the Access Key ID + Secret Access Key now — the secret is shown only once.
  6. Your S3 endpoint is https://<ACCOUNT_ID>.r2.cloudflarestorage.com.

Deriving S3 creds from any Cloudflare API token: Access Key ID = the token's id; Secret Access Key = the SHA‑256 hash of the token value.

Serve objects publicly (for <img> on the frontend)

  • Custom domain (recommended; needs the domain's zone on Cloudflare): bucket → Settings → Public access → Connect Domain → e.g. thumbs.codeamanilabs.org.
  • r2.dev URL: bucket → Settings → Public access → enable the managed r2.dev URL.
  • Both make objects world-readable. Keep private buckets behind an app proxy + a scoped read token instead.

Credentials & Permissions for Claude Code automation

What Claude Code needs to automate Cloudflare, and the gotchas that block it:

CredentialCreate atScope / permissionLets Claude Code automate
Account IDR2 Overview / dash URLidentifier (not secret)target API + S3 endpoint
Zone IDdomain → Overview (API section)identifier (not secret)DNS API calls for that zone
R2 S3 token (Access Key + Secret)R2 → Manage R2 API TokensObject Read & Write, scoped to a bucketupload/serve objects (boto3, AWS SDK, rclone, app proxy)
R2 admin tokenAccount API Tokens → CustomWorkers R2 Storage: Editcreate/list/delete buckets + settings via API
DNS tokenAccount API Tokens → CustomZone → DNS → Edit (+ Zone → Read)add/edit DNS records (subdomains, R2 custom domains)
Global API KeyMy Profile → API Tokensfull account (legacy)everything via wrangler legacy auth
OAuth (wrangler login)local browseryour user's permissionsall local wrangler commands

Automation gotchas (learned the hard way):

  • The Cloudflare MCP (claude.ai connector) manages Workers/R2 buckets/KV/D1 — but cannot edit DNS and cannot upload R2 objects. Use a DNS token for DNS and wrangler r2 object put / the S3 API for object uploads.
  • wrangler r2 object put <bucket>/<key> --remote uploads objects; auth via CLOUDFLARE_API_TOKEN (preferred) or CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL.
  • Global API Key is all-powerful — never put it in an app, repo, or CI. Prefer scoped tokens; keep the global key for local CLI only.
  • Prefer Account API tokens for unattended automation (they don't break when a user leaves). Always scope R2 tokens to specific buckets.
  • An R2 custom domain requires the domain's zone to be on Cloudflare.

Common Use Cases

Use CaseApproach
Edge API with D1Worker + wrangler d1 execute for schema
Global KV cacheKVNamespace binding in Worker
Static sitewrangler pages deploy dist/
File storageR2 bucket + Worker presigned URLs
Rate limitingCloudflare Rate Limiting via MCP API
DNS managementMCP execute DNS zone endpoints

Troubleshooting

IssueFix
CLOUDFLARE_API_TOKEN missingCreate token at dash.cloudflare.com with Workers:Edit permissions
wrangler dev port conflictUse wrangler dev --port 8788
D1 migration not applyingRun wrangler d1 migrations list my-database --remote to check state
Worker over 1MBEnable wrangler deploy --minify or split into sub-workers
KV stale readsKV is eventually consistent; use D1 for strong consistency