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 in one paragraph
Section titled “Cloudflare Pages in one paragraph”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-devops → https://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 andpackages/uiresolve)
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.
The cf-deploy.sh helper
Section titled “The cf-deploy.sh helper”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:
./scripts/cf-deploy.sh devops # build + deploy one app./scripts/cf-deploy.sh all # build all, then deploy every appIts whole job is to be a repeatable, ordered deploy so nobody does it slightly differently each time:
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 | |
|---|---|---|
| Scope | your entire Cloudflare account, all permissions | one account, Pages: Edit only |
| Bound to | the machine / your personal session | this project’s checkout |
| If leaked | attacker gets everything you can do | attacker can edit Pages on one account — nothing else |
| Wrong-account risk | high — uses whatever you last logged into | low — 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:
set -a; source .cloudflare.env; set +a # export the two vars into the environmentbunx wrangler whoami # confirm it resolves to the RIGHT account./scripts/cf-deploy.sh devopsThis 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.
Check your understanding
Section titled “Check your understanding”- 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.
- What does the
--branch=previewflag change about awrangler pages deploy, and why is that useful? cf-deploy.shwarns instead of proceeding whenCLOUDFLARE_API_TOKENis unset. What specific mistake is that guardrail preventing?- Give three concrete reasons the repo prefers a scoped API token over
wrangler loginfor deploy auth. - The deployed site holds no secrets, yet the deploy needs a token. Reconcile those two statements.
Show answers
- 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 deployyourself to push the finisheddist/. This is push-CD — you decide when to ship, no git connection required. --branch=previewmarks 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.- Without the token, Wrangler falls back to the machine-wide
wrangler loginsession — 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 thefp-*projects. - 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.envrather than your machine session. - 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).