CompanionSetup, part two: production infrastructure

Section 2 set up the agent on your machine. This page sets up everything around your machine — the places the rest of the guide quietly assumes exist. When Section 7 says "encode the definition of done in CI," when Section 8 says "every pull request gets a preview deploy," when the readiness protocol asks whether staging uses synthetic data — all of that presumes a repo host, a pipeline, a staging environment, and a backend. None of the chapters builds them. This page does.

Like Section 2, this is a READ-DO checklist: read each step, do it, check the verify block, move on. Budget about an hour — most of it is waiting for services to provision, not typing. Everything is free to start except the optional domain; where money eventually enters, the step says so honestly. The running example is the salon booking app, but every step works for any project.

For newcomers

Why your laptop isn't enough. So far the app only exists on your computer: if the disk dies, the code is gone; if you close the lid, the site is down; and no gate from Section 7 can physically stop a bad change, because nothing stands between "the agent wrote it" and "it's the only copy." Production infrastructure is four separations: a copy of the code that lives elsewhere (the repo host), a machine that re-runs your checks on every change (CI), a rehearsal copy of the app customers never see (staging), and a live copy they do (production). Each step below adds one.

Before you start

You need all of Section 2 done — not just the tooling (Node, git, pnpm, Claude Code working) but its scaffolding step, which leaves you with a real project: a package.json, TypeScript in strict mode, Vite to build it, Vitest to test it, and one passing test. That part isn't optional here. Step 2's CI runs pnpm install --frozen-lockfile, pnpm tsc --noEmit and pnpm vitest run; step 3 builds with Vite into dist; step 8 adds a pnpm seed command to package.json. Point any of those at an empty folder and they fail on the first line. You also need that folder to be a git repository with at least one commit, and an email address you can receive verification mails on. If your project has no tests yet, the CI step will still work — it just proves less until Section 5's tests exist.

One rule for the whole page: shell commands go in your terminal; prompts go inside Claude Code (the chat). Each code block below says which it is — pasting a shell command into the agent chat mostly works because the agent runs it for you, but then you haven't learned which is which, and one day the agent will "helpfully" run something you only meant to read.

1

A home for your repo — GitHub

Your code needs a copy that doesn't live on your laptop, and a place where the machinery of the next steps can attach to it. That place is GitHub.

For newcomers

What GitHub actually is. GitHub is a website that hosts git repositories — think of it as the official copy of your project, with your laptop holding a working copy you sync against. But it's more than storage, and this is why the guide's vocabulary suddenly becomes visible: when you open your repo's page, the Issues tab is where Section 4's tickets physically live; the Pull requests tab is where Section 6's reviews happen — each pull request shows the proposed change as a red/green diff; and at the bottom of every pull request sits the checks area, where the CI you build in step 2 reports green ✓ or red ✗. Until now those were words in chapters. After this step they're tabs you can click.

Create a free account at github.com/signup (username, email, password — the username becomes part of your project URLs, so pick one you can live with). Then install GitHub's command-line tool gh, which lets you create the repository without leaving the terminal — as of mid-2026 this is the simplest path. On a Mac with Homebrew:

Type this in your terminal, not into the Claude Code chat:

brew install gh
gh auth login   # choose GitHub.com, then login via browser

Windows: winget install --id GitHub.cli in PowerShell, then the same gh auth login. No Homebrew? Installers for every platform are at cli.github.com.

Now connect your project. From inside your project folder, one command creates the repository on GitHub, wires your local folder to it, and pushes your commits:

In your terminal, inside the project folder:

gh repo create salon-booking --private --source=. --push

--private means only you (and people you invite) can see it; --source=. means "use this folder"; --push uploads your commits. Prefer clicking? The web equivalent is the + menu → New repository on github.com, followed by the three connect-and-push commands GitHub shows you afterwards — same result, more typing.

You'll know it worked when gh repo view --web opens a browser page showing your files, with Issues and Pull requests tabs across the top.

From now on, "push" means "sync my commits to GitHub" — and your laptop is no longer the only copy of anything.

2

First CI workflow + branch protection

CI is a machine that re-runs your checks on every proposed change, so "the tests pass" stops being a claim the agent makes and becomes a fact GitHub displays. On GitHub, CI is called GitHub Actions, and it's configured by one file in your repo. Ask the agent to create it — this is a file inside your project, which is exactly the agent's job:

Say this inside Claude Code (it's a prompt, not a shell command):

Create .github/workflows/ci.yml with exactly this content,
then commit and push it:

…and give it this workflow (verified against current Actions syntax as of mid-2026 — the @v5/@v6 pins move roughly yearly; if a step fails with a deprecation warning, ask the agent to bump the version):

name: CI
on:
  pull_request:
  push:
    branches: [main]

jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: pnpm/action-setup@v4
        with:
          version: 10
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm tsc --noEmit
      - run: pnpm vitest run

Read it top to bottom once, because it's the whole idea of CI in twelve lines: on every pull request and every push to main, rent a fresh Linux machine, fetch the code, install pnpm and Node, install dependencies exactly as pinned in the lockfile, type-check, run the tests. Any step fails → red ✗ on the pull request. As of mid-2026, GitHub's free plan includes 2,000 Actions minutes per month for private repos (public repos are free without limits) — this workflow uses one or two minutes per run, so the allowance is plenty for a solo project.

Now make the green check required, so a red ✗ physically blocks the merge button. On your repo's page: Settings → Rules → Rulesets → New ruleset → New branch ruleset. Name it, set enforcement to Active, target the default branch, and enable Require status checks to pass, selecting the checks job. This is Section 2's hook idea at repository scale: prose asks, machinery guarantees.

The honest catch on the free plan

As of mid-2026, GitHub only enforces branch protection and rulesets on public repositories for free accounts; on a private repo you can configure the rules, but they don't bite until you're on a paid plan (GitHub Pro, individual — around $4/month; prices change). Your three real options: make the repo public (fine for many projects, wrong for some), pay for Pro, or keep the repo private and adopt one iron habit — never press merge while the check is red. The CI itself runs and reports either way; only the physical blocking is gated. For a solo builder the habit works until the day you're tired, which is exactly the day machinery earns its fee.

You'll know it worked when you ask the agent to open a small pull request and the PR page grows a "checks" section that finishes green — and when a deliberately broken change (ask the agent to make a test fail on a branch) produces a red ✗ instead.
3

Staging for free — Cloudflare Pages

Section 8's cheapest form of staging is a preview deploy per pull request: every PR gets its own private copy of the app at its own URL, and merging to main deploys production. Cloudflare Pages gives you exactly that, free, through its Git integration — this is the pipeline half of Section 13's golden-path stack (Vite + TypeScript + Playwright + Cloudflare Pages).

Create a free account at dash.cloudflare.com. Then, in the dashboard: Workers & Pages → Create → Pages → Connect to Git (labels drift; as of mid-2026 this is the flow). Authorize GitHub when asked, pick your repo, and configure the build: framework preset Vite (or set build command pnpm build, output directory dist), production branch main. Save and deploy. That's the entire setup — from now on Cloudflare watches the repo itself.

What you just bought, for nothing: every push to main deploys production at your-project.pages.dev; every pull request gets a unique preview URL, posted as a comment on the PR by Cloudflare's bot and updated on every new commit. The preview is your staging: click through the salon's booking flow there, watch the Section 6 demo reel against it, and only then merge. The free plan includes 500 builds a month and unlimited bandwidth (as of mid-2026) — a solo project rarely uses a tenth of that.

Pages or Workers? The 2026 answer

As of mid-2026, Cloudflare recommends its newer "Workers with static assets" platform for brand-new full-stack projects, and Pages has reached feature parity with it rather than the other way around. Pages remains fully supported with no sunset date, its Git flow is the simpler of the two, and it's what this guide's golden path uses — so start here. If your backend later grows serious server code on Cloudflare, migrating Pages → Workers is a documented, mechanical move, not a rewrite.

You'll know it worked when the first deploy finishes and your-project.pages.dev shows your app — and when your next pull request grows a bot comment containing a preview URL that shows that branch's version, not main's.
4

The backend decision

Here's the honest limit of everything above: Cloudflare Pages serves files. A static PWA host runs no server code — and the salon app can't stay static forever, because four of its needs have nowhere to live in a pile of files:

  • A database — bookings must exist somewhere that isn't one customer's phone.
  • Auth — the owner logs in; customers prove who they are; the browser alone can't be trusted to enforce that.
  • API endpoints — server-side code that takes "book Saturday 10:00," checks the slot is really free, and writes it, atomically (Section 7's double-booking gate lives here, not in the browser).
  • Background jobs — the reminder email the night before an appointment has to be sent by a machine that's awake when the customer isn't.
For newcomers

Frontend and backend, in one sentence each. The frontend is everything that runs on the customer's device — the pages Cloudflare serves. The backend is code and data running on computers you control, where rules can actually be enforced: a customer can fiddle with anything in their own browser, but they cannot fiddle with your server checking whether the chair is free. Any feature that involves trust, money, shared state, or things happening while nobody's looking is backend by nature.

The recommended default for beginners, as of mid-2026: Supabase. It's a managed Postgres database with auth, auto-generated APIs, file storage, and scheduled functions bundled — which means one account covers all four needs above, its docs are unusually agent-friendly, and Postgres is the least career-limiting database choice you can make. Two named alternatives, both fine: Neon (excellent managed Postgres, now under Databricks, but database only — you assemble auth and APIs yourself) and Cloudflare D1 + Workers (stays all-Cloudflare and pairs naturally with Pages, but it's SQLite-based and more assembly). If you have no strong pull toward either, take the default and move on — this is a two-way door for a young project, because the domain logic the agent writes ports.

Setup, at orientation depth — the agent does the code, you do the accounts:

  • Sign up at supabase.com (using your GitHub account keeps logins simple) and create a New project: name it, pick the region closest to your customers, and save the generated database password in your password manager — this is a real credential, not a formality.
  • Wait a couple of minutes while the project provisions, then click the Connect button at the top of the dashboard. That's where the connection strings live — the URL-shaped credential your app's server code uses to reach the database.
  • Don't paste it anywhere yet. It goes into the secret stores in step 5 — never into the repo, and never into the chat.

Then, inside Claude Code: "Wire this project to Supabase: read the connection string from the environment variable DATABASE_URL, create the bookings schema we designed, and add the Supabase client for auth." The code is the agent's job; knowing where the credential lives is yours.

Free-tier honesty

As of mid-2026, Supabase's free tier includes two projects, a 500 MB database, and generous auth — and it pauses projects after about a week of inactivity. Perfect while you're building; wrong the day a real customer's booking depends on it. Budget the Pro plan (around $25/month; prices change) as part of "real users now," not as a surprise.

You'll know it worked when the Supabase dashboard's Table Editor shows the tables the agent created, and a booking made in your local app appears there as a row.
5

Secrets — where credentials actually live

You now hold real credentials: a database connection string, API keys. Three rules, one per place a secret might exist:

  • Never in the repo. Anything committed is copied to GitHub, to every laptop that clones it, and into every agent context that reads the repo. Locally, secrets live in a file called .env that git is told to ignore.
  • In production, secrets live in the platform. Every service has an environment-variables screen: Cloudflare Pages under your project's Settings → Variables and Secrets, GitHub Actions under Settings → Secrets and variables → Actions (labels as of mid-2026). Set DATABASE_URL there once; deployed code reads it at runtime; it never touches the repo.
  • Pasted into a chat = burned. If a secret ever lands in an agent conversation, a log, or a screenshot, rotate it — generate a new one in the service's dashboard and retire the old. Agents keep transcripts; treat the chat like a postcard.

Two files make the local side mechanical. .env holds your real values and stays ignored; .env.example holds the names with placeholders and is committed, so future-you (and the agent) can see which variables the app expects without seeing any values:

Say this inside Claude Code:

Add ".env" to .gitignore. Create .env.example listing every
environment variable the app reads, with placeholder values
and a one-line comment each. Confirm .env is not tracked by git.

New machine later? In your terminal: cp .env.example .env, then fill in real values. Windows (PowerShell): copy .env.example .env.

That's the git side handled — but note what it does not do. .gitignore keeps the file out of commits; it does nothing to stop the agent opening the file and reading the credential straight into its context, where a transcript keeps it forever. The line that stops that is a deny rule in .claude/settings.json, from Section 2's permissions step. If you set it up there, the rules are already in place; check them now, because this is the moment the file they protect actually contains something:

File contents — .claude/settings.json (merge into the existing permissions block):

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)"
    ]
  }
}

Permissions denials can't be argued around — no instruction the model receives, including one smuggled in through data it reads, overrides them. Two honest limits, both covered in Section 2: the rule binds Claude's own Read tool, not a shell command like cat .env (that's what the sandbox and the pre-tool-use hook are for), and it protects the local file only. The copies you pasted into Cloudflare's and GitHub's settings screens are guarded by those accounts, which is the argument for putting a real password and MFA on both.

You'll know it worked when .env exists locally but does not appear on the repo's GitHub page; asking the agent "read .env and tell me the database URL" is refused outright rather than answered; and the deployed app still reaches the database, proving it reads the platform's copy, not a file.
6

A domain, DNS, and HTTPS

salon-booking.pages.dev works, but customers trust glowsalon.example more, and you can't print a .pages.dev URL on a gift voucher. A domain is a yearly rental, not a purchase: a .com runs roughly $10–15/year depending on registrar (prices change). Since you already have a Cloudflare account, Cloudflare Registrar is the low-friction choice — as of mid-2026 it sells domains at cost with free WHOIS privacy — but any registrar works.

Pointing it is three ideas: DNS is the internet's phone book, mapping your name to your host; a DNS record is one entry in it; TLS is the certificate that makes it https:// with the padlock. On Cloudflare the whole thing collapses to clicks: in your Pages project, Custom domains → Set up a custom domain, enter the name — if the domain is on Cloudflare, the DNS record is created for you, and the TLS certificate is issued and renewed automatically. Bought elsewhere? The same screen tells you the one record to add at your registrar. No step here involves buying, installing, or renewing a certificate by hand — on modern platforms HTTPS is automatic, and any provider that makes you handle certificates manually is telling you something.

You'll know it worked when https://your-domain loads your app with the padlock icon, usually within minutes (DNS can occasionally take a few hours to settle worldwide).
7

Write the infrastructure down — as code

Six steps of clicking got you a working setup, and every one of those clicks now exists only as a memory and a dashboard. Section 8 asks for better: the servers, database, DNS records and project settings should themselves be infrastructure as code — declared in files in your repo, not assembled by hand and remembered by nobody.

For newcomers

Why a file beats a dashboard. A console click leaves no record of what changed, who changed it, or what the setting was before. A file in the repo turns the same change into an ordinary diff on a branch: reviewable, revertable, and visible in the history six months later when you're asking "when did this DNS record change?" It also makes environments genuinely comparable — staging and production stop drifting apart, because both are built from the same definition. And it's the property Section 8's ransomware playbook quietly depends on: if an account is compromised or an environment is destroyed, you rebuild from the code rather than reconstructing six steps of clicking from memory.

The tool is OpenTofu — the open fork of Terraform, and the safer default now that Terraform's licence is no longer open source. Its files describe the infrastructure you want; tofu plan compares that description against what actually exists and prints the difference; tofu apply makes reality match. Pulumi is the equivalent if you'd rather write TypeScript than a config language. All three are formats the agent knows well, so writing the files is its job, not yours.

Install it in your terminal:

brew install opentofu   # Windows: winget install --id OpenTofu.Tofu
tofu version

Now have the agent describe what you already built. This is the gentle way in: you're not creating new infrastructure, you're writing down infrastructure that exists, which means a mistake shows up as a plan that wants to change something — before anything is touched.

Say this inside Claude Code:

Create an infra/ directory with OpenTofu config describing the
infrastructure this project already has:

- the Cloudflare Pages project, its production branch and its
  build settings
- the Cloudflare DNS zone and the records pointing at it
- the Supabase project's settings

Use the cloudflare and supabase providers, pin both to a major
version, and put every credential and account ID in variables
read from the environment — no secrets in the files. Then run
"tofu plan" and show me the output. Do not run apply.

Two housekeeping details the agent will raise. State: OpenTofu keeps a record of what it believes exists, and that file contains secrets, so it does not go in git — start with it on your machine, and move it to a remote backend (Cloudflare R2 and Supabase's storage both work) when a second person or a CI job needs to run apply. Credentials: the API tokens OpenTofu needs are secrets like any other and belong in .env and the platform stores from step 5, never in infra/.

Where to stop

This step is deliberately partial, and Section 8 says so too: declare the pieces that matter — DNS, project and build settings, environment configuration — and leave the genuinely stateful corners alone for now. Your database schema is already handled as code by migrations, which is the right tool for it; OpenTofu manages the project the database lives in, not the tables inside it. Partial infrastructure as code is a large improvement over none, because the parts that are declared stop drifting. This is a solo-operator setup on managed platforms — if a walkthrough starts talking about clusters and control planes, you've wandered into a different problem than the one you have.

You'll know it worked when tofu plan reports no changes against your live setup — the files and the dashboards agree — and then, as the real proof: change one value in the files (a DNS record's target, say), run tofu plan again, and see exactly that one change listed and nothing else. Revert the edit. A plan that wants to change things you didn't touch means the files don't yet describe reality.
8

Seed data & fixtures

A fresh staging deploy of the salon app is an empty shell — no stylists, no services, no bookings — and you can't test a booking flow against nothing. The fix is a seed script: a program, committed to the repo like any other code, that fills a database with realistic fake data. Committed matters: a fresh environment becomes usable with one command, forever, instead of depending on whoever once clicked test data together by hand.

Say this inside Claude Code:

Create scripts/seed.ts: three stylists, six services, and two
weeks of varied fake bookings — include edge cases (a fully
booked Saturday, an appointment crossing a DST change). All
names and emails clearly synthetic. Add "pnpm seed" to
package.json, reading the target database from DATABASE_URL.

One rule rides along, and it's a gate you'll meet again in the readiness protocol (Gate 57): staging and dev use anonymised or synthetic data — never a raw copy of production. Staging is precisely the environment agents and half-tested code touch most; the day it holds real customers' names and phone numbers, every staging bug is a privacy incident. Synthetic data — invented from nothing, as the seed script above does — is the cleanest answer and costs the agent five minutes. Anonymised data is the other legitimate route, and it's the harder one: a copy of production with every personal field genuinely scrubbed, not merely renamed or hidden by a query. If you ever build a "copy production to staging" script, that scrubbing is the part to verify, because a copy that only looks anonymised fails this gate while appearing to pass it.

You'll know it worked when running pnpm seed against the staging database fills the app with a plausible fake salon — and the calendar shows that fully booked Saturday.
9

What this costs

The short version: building is free; being depended on costs a little. Rough monthly picture as of mid-2026 — free tiers exist at every layer, prices change, so treat these as ranges to budget with, not quotes:

Hobby scale — building, friends testing

GitHub Free: $0. GitHub Actions within free minutes: $0. Cloudflare Pages: $0. Supabase free tier: $0. Domain: roughly $10–15/year. Total: about $1/month. The one real cost at this stage is your Claude plan — the infrastructure itself is effectively free.

First thousand users — people depend on it

Supabase Pro (no more auto-pausing): around $25/month. GitHub Pro, if you want enforced protection on a private repo: around $4/month. Cloudflare: usually still $0 at this size. Email/SMS for booking reminders: roughly $0–20/month depending on volume. Total: roughly $30–50/month — around one haircut. The jump isn't gradual: it happens the day real customers rely on the app, and it buys exactly the guarantees (always-on database, enforced gates) that "real customers" requires.

That's the hour. Take stock of what exists now that didn't this morning: the code lives in two places, every change is re-checked by a machine and reported on the pull request, every PR gets a private rehearsal URL, main deploys itself to production, credentials live in vaults instead of files, and one command can furnish an empty environment with a fake salon. Sections 7 and 8 assume everything on this page exists. When a gate mentions CI, staging, or backups — this is where they came from. The backup and restore-rehearsal gates from Section 8 and the protocol have a home now too: your code is covered by the repo host, and your database provider handles daily backups on paid tiers — verifying you can actually restore one is the game-day rehearsal Section 8 asks for.

Quick check: a red ✗ appears on your pull request, but the agent insists the code is fine. Who's right, and what do you do?

The ✗ is right until proven otherwise — that's the entire point of moving checks off your laptop. Click Details next to the failed check, read the log (the failing step names itself), and paste the error into the agent to fix. What you never do is merge past a red check "because the agent said it's fine": the agent's claim is exactly what CI exists to test, and Section 1's evidence-over-claims rule doesn't bend for confident claims. Occasionally the check itself is broken — a version pin gone stale, a flaky install. That's still not a reason to merge on red; it's a reason to fix the workflow file, which is code like any other.