Tutorials Ultimate Web Development Series Chapter 25

Local CI vs Online CI — What ci:local Means, the Pros and Cons, and Every Place You Can Run the Checklist

WebChapter 25 of the Ultimate Web Development Series25 minJune 7, 2026Beginner

Ch 15 taught you what CI is and how to write a workflow. Ch 21 priced it out and proved most of it is free. Ch 24 did the buy-vs-rent math for the one expensive case — macOS builds — and landed on a headless Mac mini.

All three of those chapters kept brushing past the same two words without ever stopping to define them properly:

ci:local — run the exact same checklist a server would run, but on the computer already in front of you, before you push.

This chapter stops and looks at it head-on. What "local CI" actually is, the one rule that keeps it honest, a real pros-and-cons (not a sales pitch), every place you can run the checklist — on your own machine and in the cloud — and how the two layers stop competing and start covering for each other. We'll use this project's own setup as the worked example throughout, because it ships a real one.

First, What "ci:local" Actually Means

There's nothing mystical about CI. Strip away the dashboards and the green checkmarks and it is one sentence: run a checklist of commands on a clean copy of the code, and if any of them fail, something is broken. Ch 15 called that checklist a workflow; the commands are usually lint, type-check, test, build.

"Online CI" runs that checklist on a rented computer in a data centre after you push. "Local CI" runs the very same checklist on your laptop, before you push. That's the whole distinction. Same commands, different machine, different moment.

The name comes straight from how npm scripts are written (see Ch 20). The colon in ci:local isn't an operator — it's just a naming convention for grouping related scripts. Plenty of teams add a script literally called this:

// package.json — the convention some teams use
{
  "scripts": {
    "ci:local": "npm run lint && npm run typecheck && npm test && npm run build"
  }
}

So npm run ci:local becomes "run the whole CI checklist, here, now." This project doesn't spell it ci:local — it calls the same idea check — but the concept is identical, and we'll get to the real one in a moment.

The Same Checklist, Two Places It Can Run

Here is the mental model the rest of the chapter hangs on. One checklist. Two places it can execute. Two very different feedback speeds.

Loading diagram…

Figure 1 — The same checklist feeds two lanes. Local is fast, free, and private (you fix things before anyone sees them). Online is slower and metered but enforced and shared. They are not rivals; they're the same checklist run twice for two different reasons.

The reason both lanes are worth having comes down to feedback speed. The earlier a mistake is caught, the cheaper it is to fix — an idea the industry calls shifting left (catching problems further "left" on the timeline, nearer the keyboard). Your editor catches typos in milliseconds. ci:local catches a failing test in seconds. Online CI catches it in minutes — after you've pushed, switched tasks, and started thinking about something else.

Loading diagram…

Figure 2 — The cost of a bug grows as it moves right. ci:local exists to catch things one stop earlier than online CI, so the slow, metered lane almost never has to say "no."

This Project's Real ci:local

Enough theory — this project ships a working local-CI gate, and reading it teaches more than any invented example. Both the website and the backend define a single check script that is their local CI.

The website (website-next/package.json):

// website-next/package.json
{
  "scripts": {
    "check": "npm run lint && npm run typecheck && npm run check:stripe-links && npm run check:krea-credentials && npm run check:content-coverage",
    "lint": "eslint src",
    "typecheck": "tsc --noEmit",
    "check:stripe-links": "node scripts/check-stripe-links.mjs",
    "check:krea-credentials": "node scripts/check-no-hardcoded-krea-key.mjs",
    "check:content-coverage": "node scripts/check-content-coverage.mjs"
  }
}

The backend (saas/package.json) does the same thing with its own rules:

// saas/package.json (abbreviated)
{
  "scripts": {
    "check": "npm run check:json-readers && npm run check:json-responses && npm run check:endpoints && npm run check:sql-select-star && npm run check:migrations && …"
  }
}

Notice what those check:* scripts are. They aren't generic — each one is a project rule turned into code that fails loudly:

GuardThe rule it enforces
check:stripe-linksEvery Stripe Payment Link in the code matches the approved set — no stray or mistyped checkout URLs.
check:krea-credentialsNo hard-coded Krea API key ever lands in the repo (the provider is retired; the secret must stay dormant).
check:content-coverageEvery MDX tutorial is registered in the route/slug maps the sitemap reads — so a new chapter can't be invisible to Google.
check:endpoints (saas)Every API route is registered where it should be — no orphan handlers.
check:sql-select-star (saas)No SELECT * in D1 queries — columns are listed explicitly.

So for this project, ci:local isn't aspirational — it's cd website-next && npm run check and cd saas && npm run check, run by a human before deploying. (If you prefer the literal name, "ci:local": "npm run check" is a one-line alias.)

Mini-exercise

Open any project you have with a package.json. Is there a single script that runs all the checks in one go? If not, that's the gap this chapter is about. Jot down the commands you'd want it to run — that list is your ci:local.

The One Rule That Makes It Work: One Script, Two Callers

Here is the single most important idea in the chapter, and the thing most beginners get wrong. Local CI and online CI must run the same script — not "roughly the same steps," the same named command.

The failure mode is drift. If your laptop runs npm run lint && npm test but the GitHub Actions workflow runs eslint . && vitest run --coverage, the two will slowly diverge. One adds a type-check; the other doesn't. Green locally, red online — or worse, green in both while actually checking different things. Now CI is a source of confusion instead of confidence.

The fix is mechanical: define the checklist once, as a named script, and have both callers invoke that one name.

# .github/workflows/ci.yml — the online caller
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm run check    # ← the SAME command you run locally
# Your terminal — the local caller
npm run check                 # ← identical

Now there is exactly one definition of "passing." The server and your laptop can never disagree about what they check, only about where they run it. When you fix the checklist, you fix it in one place (package.json) and both callers pick up the change for free.

Loading diagram…

Figure 3 — One script, two callers. This is the "source of truth" idea from Ch 23 applied to your checklist: the check script is authoritative, and both your laptop and the server merely call it. No drift is possible because there's only one definition.

Local CI: The Honest Pros and Cons

Local CI is wonderful, and it is not a complete safety net. Selling it as either extreme is how people end up burned. Here's the real ledger.

✅ Pros of running the checklist locally⚠️ Cons of relying on local only
Fast. Seconds. No push, no queue, no VM boot, no log-fetching round trip.Not enforced. Nothing stops you pushing without running it. Discipline is the only gate.
Free. Your hardware, zero metered minutes (the whole point of Ch 21)."Works on my machine." Your laptop isn't a clean room — a stray global install or different Node version can hide a break.
Offline. Works on a plane, on bad hotel Wi-Fi, anywhere.No shared record. There's no green check on a PR, no log a teammate or reviewer can point to.
Tight loop. You fix the problem before anyone — human or server — ever sees it. Maximum "shift left."Can't vouch for others. It only proves your machine is happy, never a contributor's.
Full power. Your real toolchain, GUI debuggers, and the slow macOS build with no 10x runner tax (Ch 24).Easy to skip under pressure. "It's a tiny change, I'll skip the checks" is exactly when things break.

The cons all share one root: local CI is advice, not a law. It can be ignored, and it speaks only for one machine. That's precisely the gap online CI fills.

Online CI: The Honest Pros and Cons

The same ledger from the other side.

✅ Pros of running the checklist online⚠️ Cons of relying on online only
Enforced. It's the merge gate — you literally cannot merge a red PR (when branch protection is on).Costs minutes. Metered, and macOS bills at 10x (Ch 24).
Clean room. A fresh VM every run catches the "works on my machine" bugs your laptop hides.Slow feedback. Push → queue → boot → run → fetch logs. Minutes, after you've moved on.
Shared truth. A visible green check, full logs, and an audit trail everyone can see.Needs the network up. Provider outage or no connection = no CI.
Safe for strangers' code. Runs untrusted PRs in a throwaway sandbox, not on your machine.Secrets overhead. Tokens and keys must be stored and scoped in the provider.
Always-on. Runs automatically on every push, even while you sleep.Setup + maintenance. A YAML file to write, debug, and keep current.

Read the two tables together and the punchline writes itself: the pros of one are the cons of the other. Local is fast/free/private but unenforced and single-machine. Online is enforced/clean/shared but slow, metered, and networked. You don't pick a winner — you use each for what it's good at.

Where You Can Run It — Local-Host Options

"Run it locally" isn't a single thing. There's a ladder from "type the command yourself" to "automate it so it's impossible to skip." From simplest to most powerful:

OptionWhat it isBest for
Run it by handnpm run check before you push. The honest baseline.Solo, just starting. Costs nothing, requires you to remember.
Native git hookA pre-push script in .git/hooks/ that runs the checklist and aborts the push if it fails. Not committed to the repo.Solo automation with zero dependencies.
HuskyStores hooks in a committed .husky/ folder so the whole team shares the same gate. The popular default in JS projects.Teams that want everyone to get the hook on clone.
lefthookA single fast binary that runs hooks in parallel; config in one lefthook.yml. Language-agnostic.Bigger hook setups where speed and parallelism matter.
pre-commit (framework)The pre-commit.com tool — a YAML-defined registry of hooks across many languages.Polyglot repos (Python + JS + shell) wanting one hook manager.
Makefile / just / TaskA task runner that names your commands (make check, just check) independent of npm.Repos that aren't only JavaScript, or want one entry point across tools.
actRuns your actual GitHub Actions workflow file locally in Docker. Linux runners only.Debugging a broken workflow without the push → wait → red loop.
Docker (same image)Run the checklist inside the same container the server uses, so "clean room" happens locally too.Killing "works on my machine" without leaving your laptop.
Self-hosted runnerYour machine registered with GitHub Actions: their dashboard and gate, your hardware, no metered minutes. The Mac-mini move from Ch 24.Heavy or macOS builds, dodging the 10x tax while keeping the online gate.
Watch modevitest --watch, tsc --watch — re-runs the relevant check the instant you save.The innermost loop, while you're actively writing the code.

Notice the self-hosted runner sits on the seam. It's "local hardware" but "online orchestration" — your Mac mini does the compute, GitHub provides the dashboard and the gate. That hybrid is the bridge to the next section.

Where You Can Run It — Online Options

Every online CI sells the same thing — minutes on a VM it boots and destroys for you (Ch 21). They differ in free tier and what they pair best with:

ServiceWhat it's best at
GitHub ActionsThe default if your code is on GitHub. Free + unlimited on public repos; generous free tier on private. The gate most of this series assumes.
GitLab CIThe built-in option if your code lives on GitLab. Same model, tightly integrated.
Cloudflare Workers BuildsBuild + deploy on push for Workers / Next-on-OpenNext — this project's stack. Push-to-deploy with no YAML, but it's a deploy pipeline, not a merge gate (Ch 24).
Vercel / NetlifyHost + CI in one for frontend apps: preview deploy per PR, build on push. Great for Next.js / JAMstack.
CircleCIStandalone CI with a generous free credit pool; common in teams not centred on GitHub Actions.
Bitrise / CodemagicMobile-first CI with managed macOS runners, code signing, and TestFlight upload baked in — the iOS angle (see the Ship iOS CI chapter).
Xcode CloudApple's own CI, wired into Xcode and App Store Connect. The path of least resistance for Apple-platform apps.
JenkinsThe self-managed classic: you run the server. Maximum control, maximum maintenance. Mostly enterprise now.

For this project's web stack the answer is small: GitHub Actions for the gate, Cloudflare Workers Builds for the deploy. The mobile services matter only because SimpleAppShipper also ships a macOS app — which is exactly why its one workflow file is about macOS.

Local vs Online, Side by Side

The decision table, distilled. This is the "pros and cons" of the two locations in one view:

Dimension💻 Local (ci:local)☁️ Online CI
Speed of feedbackSecondsMinutes (push → queue → boot → run)
CostFree (your hardware)Metered minutes (macOS = 10x)
Enforced?No — you can skip itYes — the merge gate
Clean-room (catches "works on my machine")No (unless Docker)Yes — fresh VM each run
Works offlineYesNo
Safe for strangers' codeNoYes — throwaway sandbox
Shared record / audit trailNoYes — visible check + logs
Setup effortMinimal (one script)A YAML file + secrets
Runs automaticallyOnly via a git hookYes — every push

Look down the two columns: there is no row where one wins on everything. Local owns speed, cost, offline; online owns enforcement, clean room, trust, shared record. That shape is the argument for using both.

How They Fit Together — Not Either/Or

The trap is treating this as a choice. It isn't. The strongest, cheapest setup runs the same checklist as concentric layers, each catching what slipped past the one before:

Loading diagram…

Figure 4 — Belt and suspenders. ci:local (green) catches the overwhelming majority in seconds, so the metered online gate (blue) rarely has to fail — but it's there to enforce the rule and catch the clean-room bugs your laptop hid. Same npm run check at every stage.

The division of labour is clean:

When local does its job, online almost never goes red — which keeps your metered minutes near zero and the feedback loop near instant. That's the win.

What simpleappshipper.com Actually Does

Keeping with the honesty of Ch 21 and Ch 24 — here's this project's real arrangement, which happens to use every layer in this chapter:

So the honest summary: the deployer, developer, and tester are the same person, so local CI does the real work and costs $0. The setup graduates to an automatic online gate the day a second person can push to main, or the day the repo opens to outside contributors — at which point "enforced, clean room, shared check" stops being optional. Until then, local-first isn't a compromise; it's the right amount of machinery.

Mental Model — Three Sentences

  1. CI is one checklist; "local" and "online" are just where and when you run it — your machine before the push, or a rented VM after — so the smart move is to run the same checklist in both places.
  2. Define the checklist once as a named script (npm run check) and have both your laptop and the workflow file call that one name — same words, no drift, ever.
  3. Local CI buys you speed and costs nothing but can't enforce anything; online CI enforces the rule and catches clean-room bugs but costs minutes — they're complements, not competitors, and a good git hook means online almost never goes red.

Try It Yourself (10 Minutes)

  1. In any project with a package.json, add a single script that chains your checks: "check": "npm run lint && npm test && npm run build". Run npm run check. Congratulations — that's ci:local.
  2. Turn it into a pre-push git hook (the script is in Ch 21), chmod +x it, then deliberately break a test and try to push. Watch git abort the push. Fix it; push goes through.
  3. If you have a .github/workflows/ file, make its run step say npm run check — the exact command from step 1. Now local and online physically cannot disagree about what "passing" means.
  4. (Optional) Install act, run act in that repo, and watch your real workflow execute locally in Docker — no push, no minutes, no waiting.

You now know not just how CI works, but the two places it can run, what each is good and bad at, and how to wire them so they cover for each other.

Where This Lands in the Series

That closes the operational-workflow track at full strength: you can write a CI pipeline, lint and test inside it, open and review PRs against it, read its file types, price it, host it cheaply on a Mac mini, and run the whole thing locally for instant, free feedback — with one script that keeps local and online honest.

Everything so far has been about the plumbing around your code. Part 3 finally turns to the code itself: Ch 26 — Why JavaScript Needs a Framework. What plain <script> tags couldn't do, what React actually solved, and why those frameworks are exactly where build steps get heavy enough that the local-vs-online CI tradeoff in this chapter starts to really bite.

Ch 24: Workers Builds vs GitHub Actions — and the Headless Mac MiniComing Soon →
Production WebProduction Web Apps SeriesProduction patterns for web apps: caching, rate limiting, webhooks, queues, cron jobs, and idempotency.Astro + Next.jsAstro & Next.js SeriesStatic and hybrid web app patterns with Astro, Next.js, MDX, dynamic routes, and Cloudflare deploys.CloudflareCloudflare Feature FocusFocused Cloudflare tutorials for Workers, R2, Stream, Durable Objects, and edge deployment.

Ship your apps faster

When you're ready to publish your Swift app to the App Store, Simple App Shipper handles metadata, screenshots, TestFlight, and submissions — all in one place.

Try Simple App Shipper
5 free articles remainingSubscribe for unlimited access