Skip to content

Cloudflare Pages & Wrangler

The build produced a dist/. This page takes it the last mile: onto Cloudflare Pages, the CDN that becomes the site’s runtime. Two things to understand — how the files get there (two paths, and when to use each) and how the deploy authenticates (a decision with real security weight).

Cloudflare Pages is a static-site host built on Cloudflare’s global edge. You hand it a dist/ folder and it serves those files from every Cloudflare data center, over HTTPS, with the security and cache headers from your _headers file applied automatically. Each site is a Pages project. This repo has one project per app — apps/devops → project fp-devopshttps://fp-devops.pages.dev — nine projects for nine books (the hub is the exception, named first-principle). The project name drives the free <name>.pages.dev URL and is the deploy target.

Two ways to ship: Git integration vs direct upload

Section titled “Two ways to ship: Git integration vs direct upload”

There are two deployment paths, and the repo’s DEPLOYMENT.md documents both. They are the static-host echo of the push-CD vs pull-CD distinction from GitOps.

Option A — Git integration (dashboard, “click deploy”)

Section titled “Option A — Git integration (dashboard, “click deploy”)”

You connect the GitHub repo to each Pages project once, in the Cloudflare dashboard, with a build command and output directory:

  • Build command: bun install && bunx turbo run build --filter=devops
  • Build output directory: apps/devops/dist
  • Root directory: / (the repo root — needed so the Bun workspace and packages/ui resolve)

After that, Cloudflare watches the repo: every push to main triggers a build-and-deploy on Cloudflare’s runners, and — the part that matters most — every pull request gets its own preview URL automatically. This is the pull-style model: the platform reacts to git. It is the recommended path, because the source of truth is the repo and no human runs a deploy by hand.

Option B — Direct upload with Wrangler (the CLI)

Section titled “Option B — Direct upload with Wrangler (the CLI)”

Wrangler is Cloudflare’s CLI. Here you build locally (or in your own CI) and push the finished dist/ up yourself. Each app’s package.json carries the exact command:

"deploy": "bunx wrangler@latest pages deploy dist --project-name=fp-devops",
"deploy:preview": "bunx wrangler@latest pages deploy dist --project-name=fp-devops --branch=preview"

wrangler pages deploy dist uploads the folder to the named project; the first upload creates the project if it doesn’t exist. The --branch flag is how Cloudflare distinguishes a production deploy (the project’s production branch) from a preview deploy (any other branch name), which gets its own URL and never touches the live site. This is the push-style model: you decide when to ship. It needs no GitHub connection, which is why it’s the fallback for a repo that isn’t wired to the dashboard.

Typing the build-then-deploy dance for nine apps is exactly the kind of manual, error-prone step this book exists to kill. scripts/cf-deploy.sh wraps it:

Terminal window
./scripts/cf-deploy.sh devops # build + deploy one app
./scripts/cf-deploy.sh all # build all, then deploy every app

Its whole job is to be a repeatable, ordered deploy so nobody does it slightly differently each time:

Terminal window
deploy_one() {
local app="$1"
echo "==> building $app"
bunx turbo run build --filter="$app" # build just this app (Turbo caches the rest)
echo "==> deploying $app"
( cd "apps/$app" && bun run deploy ) # run that app's wrangler deploy script
}

Note what the script does before it builds anything: it loads credentials from .cloudflare.env if present, and if the token is missing it warns you rather than silently falling back to some other account. That warning is a small guardrail against the classic mistake — deploying to the wrong Cloudflare account because your shell happened to be logged into it.

The auth decision: a scoped token, not a global login

Section titled “The auth decision: a scoped token, not a global login”

This is the most security-relevant choice in the Part, and the reason .cloudflare.env.example exists. There are two ways Wrangler can authenticate:

wrangler login (OAuth).cloudflare.env API token
Scopeyour entire Cloudflare account, all permissionsone account, Pages: Edit only
Bound tothe machine / your personal sessionthis project’s checkout
If leakedattacker gets everything you can doattacker can edit Pages on one account — nothing else
Wrong-account riskhigh — uses whatever you last logged intolow — the token names the account explicitly

The repo deliberately chooses the token. You create a token scoped to Account → Cloudflare Pages → Edit, drop it and the account ID into .cloudflare.env (which is gitignored — it must never be committed), and cf-deploy.sh sources it so Wrangler uses that identity instead of your machine-wide login:

Terminal window
set -a; source .cloudflare.env; set +a # export the two vars into the environment
bunx wrangler whoami # confirm it resolves to the RIGHT account
./scripts/cf-deploy.sh devops

This is least privilege from Part 8, made concrete: the deploy credential can do exactly one thing (edit Pages), on exactly one account, and lives only in an ignored file scoped to this project. It is also the difference between a static site’s security surface and a running service’s — the token is a deploy-time secret, used on your machine or in CI, and it is the only secret in the whole system. The deployed site itself holds none, because (as the model showed) it computes nothing at request time.

  1. Describe the two deployment paths (Git integration vs Wrangler direct upload) and match each to the push-CD / pull-CD distinction from the GitOps page.
  2. What does the --branch=preview flag change about a wrangler pages deploy, and why is that useful?
  3. cf-deploy.sh warns instead of proceeding when CLOUDFLARE_API_TOKEN is unset. What specific mistake is that guardrail preventing?
  4. Give three concrete reasons the repo prefers a scoped API token over wrangler login for deploy auth.
  5. The deployed site holds no secrets, yet the deploy needs a token. Reconcile those two statements.
Show answers
  1. Git integration (Option A): connect the repo to Cloudflare once; Cloudflare watches git and builds/deploys on every push and PR. This is pull-CD — the platform reacts to the repo, the source of truth. Wrangler direct upload (Option B): you build and run wrangler pages deploy yourself to push the finished dist/. This is push-CD — you decide when to ship, no git connection required.
  2. --branch=preview marks the deploy as a preview (non-production) deploy: it gets its own URL and does not replace the live production site. Useful for reviewing a change in a real Cloudflare environment before promoting it to production.
  3. Without the token, Wrangler falls back to the machine-wide wrangler login session — which may be a different Cloudflare account. The warning prevents deploying to the wrong account (e.g., a personal one) instead of the account that owns the fp-* projects.
  4. Any three: it’s scoped to Pages: Edit only (not full-account access); it names the account explicitly so you can’t deploy to the wrong one; if leaked it exposes only Pages-edit on one account, not everything; it’s bound to this project’s ignored .cloudflare.env rather than your machine session.
  5. The token is a deploy-time credential — used on your laptop or CI runner to upload files. The running site needs no secrets because it does no server-side work at request time; it just serves static files. So “no runtime secrets” and “a deploy needs a token” describe two different moments (serving vs shipping).