Claude Code + Cloudflare Workers

Two things to install once, then you're off:

npm install -g wrangler
wrangler login

That opens a browser and connects the Cloudflare CLI to your account. Sign up if you don't have one. It's free.

Then drop the file below into your project as CLAUDE.md. It's the map Claude Code uses to get around this kind of app: the architecture, the commands, the one gotcha that bites everyone (your local and production databases don't talk to each other), and the conventions worth keeping. With it in place, you can open a fresh folder and just start describing what you want.

The CLAUDE.md File (copy this into your project)

# Simple Cloudflare Web App

A pattern for tiny, free-to-host web apps: one Worker, one database, a folder of static
files, deployed with one command. No framework, no build step, no server to maintain.
This file documents the toolstack and conventions so you can work in any app built this way.

## Stack at a glance

| Concern        | Tool                          | Notes                                            |
| -------------- | ----------------------------- | ------------------------------------------------ |
| Compute / API  | Cloudflare Workers            | Single `fetch` handler acts as router + backend  |
| Database       | Cloudflare D1 (SQLite)        | Bound to the Worker as `env.DB`                   |
| Frontend       | Static files in `public/`     | Plain HTML/CSS/JS, served via the `ASSETS` binding |
| Tooling / CLI  | Wrangler                      | Dev server, DB migrations, secrets, deploy        |
| Hosting        | `*.workers.dev` or custom domain | Free tier covers everything at this scale      |

**No build step.** The frontend is hand-written HTML/JS served as-is. The Worker is a single
`.js` (or `.ts`) file. There's no bundler, no `dist/`, no `npm run build`. Edit a file, run `wrangler deploy`.

## Architecture

```
Browser ──> Worker (src/worker.js)
              |-- GET /            -> serve public/index.html via env.ASSETS
              |-- GET/POST /api/*  -> JSON handlers, read/write env.DB
              `-- everything else  -> fall through to static assets
```

The Worker is the single entry point. It inspects `new URL(request.url).pathname`, routes
`/api/*` to JSON handlers, and forwards everything else to the static `ASSETS` binding.
State lives in D1; the frontend talks to the app exclusively through `/api/*` endpoints.

### Worker shape

```js
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    // 1. handle OPTIONS / CORS
    // 2. route /api/* to handlers, passing `env`
    // 3. otherwise: return env.ASSETS.fetch(request)
  }
};
```

- `env` carries every binding: `env.DB`, `env.ASSETS`, and any secrets (e.g. `env.ADMIN_KEY`).
- Use the **Web platform API** (`fetch`, `Request`, `Response`, `URL`, `crypto.subtle`,
  `TextEncoder`). Node built-ins (`fs`, `path`, `process`) are **not** available unless you
  set `compatibility_flags = ["nodejs_compat"]`, so prefer not needing them.
- D1 query pattern: `await env.DB.prepare("SELECT * FROM t WHERE id = ?").bind(id).all()`.
  Always use `.bind()` for parameters, never string-interpolate user input into SQL.

## Project layout

```
.
├── public/
│   └── index.html        # whole frontend (often a single file)
├── src/
│   └── worker.js         # router + API + static-asset fallthrough
├── schema.sql            # D1 table definitions (CREATE TABLE IF NOT EXISTS ...)
├── seed.sql              # optional local sample data
├── wrangler.toml         # Cloudflare config: bindings, name, entry point
├── .dev.vars             # local-only secrets (gitignored, never deployed)
└── package.json          # npm scripts wrapping wrangler
```

## Configuration (`wrangler.toml`)

```toml
name = "<app>"
main = "src/worker.js"
compatibility_date = "2025-01-01"   # pin a date; controls runtime behavior

[assets]
directory = "./public"
binding = "ASSETS"

[[d1_databases]]
binding = "DB"
database_name = "<app>-db"
database_id = "..."                  # from `wrangler d1 create`
```

- **Bindings are how the Worker reaches resources.** A binding name (`DB`, `ASSETS`) becomes
  a property on `env`. Add a new resource, add a binding, and it shows up on `env`.
- `database_id` comes from `wrangler d1 create <app>-db` and must be committed for deploys to work.

## Secrets

Two places, never mixed up:

- **Local dev:** put secrets in `.dev.vars` (`KEY=value`, gitignored). Wrangler loads them
  into `env` automatically when you run `wrangler dev`.
- **Production:** `wrangler secret put KEY` (prompts for the value, stores it encrypted in
  Cloudflare). Secrets are **not** in `wrangler.toml` and never committed.

Same `env.KEY` accessor in code regardless of source.

## Commands

Conventionally wrapped as npm scripts in `package.json`:

```jsonc
{
  "scripts": {
    "dev":           "wrangler dev",
    "db:init":       "wrangler d1 execute <app>-db --local  --file=schema.sql",
    "db:seed":       "wrangler d1 execute <app>-db --local  --file=seed.sql",
    "db:reset":      "rm -rf .wrangler/state/v3/d1 && wrangler d1 execute <app>-db --local --file=schema.sql && wrangler d1 execute <app>-db --local --file=seed.sql",
    "db:query":      "wrangler d1 execute <app>-db --local  --command",
    "deploy":        "wrangler deploy",
    "deploy:schema": "wrangler d1 execute <app>-db --remote --file=schema.sql"
  }
}
```

One-time setup:

```bash
npm install -g wrangler        # or use the local devDependency
wrangler login                 # browser auth against your Cloudflare account
wrangler d1 create <app>-db    # prints the database_id, paste it into wrangler.toml
```

## Local vs. Remote: the #1 Gotcha

D1 has **two separate databases** and `wrangler` defaults differ by command:

- `--local` is a SQLite file under `.wrangler/state/`. This is what `wrangler dev` uses.
- `--remote` is the real production D1 in Cloudflare.

They do **not** sync. Schema or seed changes you make locally are invisible in production
until you re-run with `--remote`, and vice-versa. After deploying for the first time (or after
any schema change), apply the schema remotely:

```bash
wrangler d1 execute <app>-db --remote --file=schema.sql
```

Same trap applies to data: `--local` edits never reach users; `--remote` edits hit live data.

## Deploy

```bash
wrangler deploy            # uploads Worker + public/ assets, prints the live URL
```

The first deploy gives you `https://<app>.<your-subdomain>.workers.dev`. A custom domain is
optional: Cloudflare dashboard > your Worker > Settings > Domains & Routes (free if the
domain's nameservers are on Cloudflare).

## Schema changes / migrations

There's no migration framework. Keep `schema.sql` idempotent (`CREATE TABLE IF NOT EXISTS`,
`CREATE INDEX IF NOT EXISTS`) so re-running it is safe. For column additions on an existing
deployment, run an explicit `ALTER TABLE` against both `--local` and `--remote`, and note it
as a comment in `schema.sql` so the history is visible:

```bash
wrangler d1 execute <app>-db --remote --command="ALTER TABLE t ADD COLUMN x TEXT"
```

## Conventions

- **Validate and sanitize all input** in the Worker before touching D1. It's the only
  trust boundary. Clamp numbers, allowlist enums, reject unexpected shapes.
- **Parameterize every query** with `.bind()`; never interpolate user input into SQL.
- **Gate admin/privileged endpoints** on a secret (header or query param checked against
  `env.SOMETHING`), since there's no user auth layer by default.
- **Return JSON consistently** with a small `json(data, status)` helper and shared CORS
  headers to keep handlers terse.
- **Keep the frontend dependency-free** where possible: one HTML file, inline or co-located
  CSS/JS, `fetch()` to `/api/*`. The whole point of this stack is that there's nothing to build.

## Free-tier reality check

Workers: 100,000 requests/day. D1: 5 GB storage, millions of reads/day. For a low-traffic
personal app this is effectively unlimited, so expect about €0/month.