Cloudflare Pages, R2, D1, and Workers

Static site hosting, object storage, edge database, and serverless Workers — the full platform.

Goals

Host static sites (podcast feed, otel-explorer, Taliesin Druthers), serve audio files (R2), store OTEL traces (D1), and run lightweight edge functions (Workers) without managing servers.

Effectiveness

The platform works well once wrangler is configured. Pages deploys are fast, R2 CDN URLs are stable, D1 is a capable SQLite edge store, and Workers are suitable for simple proxies and API endpoints. Zero operational overhead once set up.

What made it effective

Friction / pain points / surprises

wrangler pages deploy defaults to preview, not production. Unless --branch=main is passed, every deploy goes to a preview URL. Custom domains serve the production deployment. We had druthers.app serving an 11-day-old broken deployment while CI reported green — every deploy was quietly going to preview. Always pass --branch=main in CI.

npx wrangler is not available in Bun-only Docker containers. npx doesn't exist when npm isn't installed. All wrangler invocations must use bunx wrangler. This caused two consecutive pipeline failures before all npx calls were replaced.

CF's native Pages build can't see private npm packages. Cloudflare's built-in CI has no access to GITHUB_TOKEN, so it can't install packages from a private GitHub registry. Every build using @taliesinsoftworks/crdtbus failed when using CF's native integration. The fix: use GitHub Actions + wrangler pages deploy instead of CF's native build pipeline.

Workers env bindings are per-request only — not available at module init. D1, KV, R2, and secrets are injected into the fetch(request, env) handler, not available as module-level globals. Patterns like const auth = createAuth(env) at the top level silently produce undefined. Initialize inside the handler or pass env explicitly.

wrangler secret put requires interactive TTY — can't be trivially scripted. The command prompts for input interactively. To use it non-interactively: echo "$SECRET_VALUE" | wrangler secret put KEY_NAME. We delegated initial secrets setup (METERED_APP, METERED_KEY for turn-worker) to Gavin to run locally.

GitHub secrets are write-only from automation. Secrets pushed to GitHub Actions via gh secret set can't be read back. If you need a token value later (e.g., to pull wrangler logs), you must get it from the original source. We had to ask Gavin to re-paste CF_API_TOKEN after setting it as a GitHub secret.

wrangler skips uploading files it considers unchanged (by content hash). If dist/ matches the previous deployment's hashes, wrangler uploads nothing and exits successfully. This produced a stale deploy after a failed prior run. Fix: always regenerate dist/ from scratch, or verify the live site after every deploy.

Pages deployment propagation isn't instant. After wrangler pages deploy exits, the new content may not be live for 30–60 seconds. Checking the URL immediately can return stale cached content — which looks like a deploy failure.

R2 bucket must be set to public explicitly. A new bucket defaults to private. Files uploaded before enabling public access return 403 at the .r2.dev URL with no meaningful error. Enable public access in the Cloudflare dashboard before the first upload.

CF_API_TOKEN needs Pages and R2 permissions separately. A token scoped only to Pages can't write to R2. The "Edit Cloudflare Workers" template covers neither. Create a custom token with Cloudflare Pages:Edit and Workers R2 Storage:Edit at minimum.