Dev, staging, prod.

How to preview WIP changes before they go public — using just branches, one repo, and a free Cloudflare feature most beginners miss.

Concept 01 — Three environments, one repo

The three faces of the same project.

Every running app or website exists in three slightly different versions, all the time:

Dev

Your laptop

localhost:3000

What you're actively editing. Lives on your Mac, runs only when you start it, dies when you close the tab.

Staging

The previewable copy

dev.your-site

A deployed version of "what's next." Lives on a real URL. Only you (and Josh) see it. Lets you click around like a real user before going live.

Prod

What visitors see

your-site.com

The public, polished version. Updated only when you've tested staging and you're sure.

Most beginners think these need to be three different repos or three different servers. They don't. They can all live in one repo, one Cloudflare Pages project, one Coolify app — distinguished only by which branch they correspond to.

Concept 02 — Branches as environments

Different branches = different URLs.

Remember from Lesson 1: a branch is just a sticky note pointing at a commit. Cloudflare Pages reads that sticky note to decide what to deploy where:

main duck-hunt.pages.dev (public, the live site)
dev dev.duck-hunt.pages.dev (your private preview)
feature/new-scoreboard new-scoreboard.duck-hunt.pages.dev (per-branch auto-URL)

Same repo. Three sticky notes. Three different deploy URLs. Whichever sticky note you move forward, the corresponding URL redeploys.

main → duck-hunt.pages.dev dev → dev.duck-hunt.pages.dev feature/x → x.duck-hunt.pages.dev PROD STAGING PREVIEW
One repo, three branches, three live URLs. CF Pages handles the mapping automatically.
Concept 03 — The magic feature you'll use forever

Cloudflare auto-deploys every branch.

This is the unlock most beginners miss. When you push any non-main branch to GitHub, Cloudflare Pages automatically:

builds it · deploys it · gives it its own URL · DMs you the link

You did nothing to configure this. It just happens. The URL pattern is {branch-name}.{your-project}.pages.dev — slugified from the branch name.

Workflow you actually live in

1. Want to try an idea? git checkout -b try-new-scoreboard

2. Make changes, commit, push.

3. CF Pages sends you a URL ~30 seconds later: try-new-scoreboard.duck-hunt.pages.dev

4. Click around, share with Josh, decide if you love it.

5. Love it? Merge the branch into main. Production URL redeploys with the change.

6. Hate it? Delete the branch. The preview URL disappears. Zero cleanup.

Why this beats "two repos for staging"

The two-repos pattern (a private repo for dev, a public repo for prod, manually copying code between them) is a relic from before branch-based preview deploys existed. You'd have to sync changes between repos, manage permissions on both, deal with diverging histories. Every modern deploy platform (Pages, Vercel, Netlify) does branch-based previews for free. One repo is the modern answer.

Concept 04 — The "should I strip CLAUDE.md from prod" worry

Repo contents ≠ deployed contents.

You wondered: "should the public prod repo strip out CLAUDE.md, .claude/, plans/, ideas/? Stronger .gitignore?"

The instinct is good, but the worry is misplaced. There are two separate things here that are easy to conflate:

What's in the repo
  • README.md
  • index.html
  • src/
  • games/
  • CLAUDE.md
  • .claude/skills/
  • plans/
  • ideas/
  • package.json
  • build.config.js
What CF Pages serves
  • index.html
  • games/duck-hunt/
  • games/pepper-popper/
  • assets/sprites.png
  • styles.css
  • app.js

CF Pages deploys whatever is in your build output directory (you configure this in the Pages dashboard — usually dist/, public/, or the repo root for simple sites). Files outside that directory — including CLAUDE.md, .claude/, plans/are never served to visitors. Visitors couldn't reach them even if they tried, because no URL maps to them.

Bottom line

Keep CLAUDE.md, .claude/, plans/, ideas/ in your repo. They're not visitor-facing. Visitors only see the build output. Even on a fully public repo, having these files is fine — many indie devs explicitly ship their .claude/ setup publicly so others can clone the working pattern.

What you DO need to strip

Secrets, always. .env, .mcp.json with credentials, any file with API keys or tokens. These belong in .gitignore + your deploy platform's environment variable system (CF Pages calls these "Environment Variables and Secrets" — set them in the dashboard, not in committed files).

Concept 05 — Public or private repo?

It almost never matters. Until it does.

For solo / small-team work, the public-vs-private question is usually overthought. Here's the actual decision rule:

Default to private

Until you have a specific reason to make a repo public, leave it private. Costs nothing. Reduces "did I commit a secret?" anxiety. You can flip private→public later with one click; flipping back to private doesn't ungoogle the history.

Reasons to make a repo public

You want others to fork it. You want to ship it as open source. You want potential employers to find it. You want easier sharing (no auth wall when sending a link). You want GitHub's free CI minutes (private repos have a smaller quota; public repos are unlimited).

Either way, the "secrets discipline" is the same

Whether private or public, never commit secrets. Private just means the blast radius of an accidental commit is smaller (just you and your collaborators saw it, not the entire internet). It's still a leak — rotate the credential the moment you notice.

✦ Lesson 4 recap

The new mental model…