How this is builtGroundworkHow this is built
23 / 26 written chapters

A4 · Part 3 · How it holds up

How code reaches readers

A branch, a preview, a merge, and a daily job that deletes what Vercel would otherwise keep forever.

7 min read1 diagram5 sections
05On this page
  1. The path, end to end
  2. What a deployment runs
  3. vercel.json, in full
  4. Why the storage filled up
  5. Knowing when something breaks

The path, end to end

There is no release process separate from Git. Work happens on a branch named after the feature, such as feature/playground, feature/whiteboard, feature/problems-page or, for these chapters, feature/architecture. Commits pile up locally over the day and are pushed once, when the work is final. That habit is about more than tidiness, and the storage section below explains why.

Vercel's Git integration watches the repository. Every pushed branch gets a preview deployment with its own URL, built exactly as production would be. A push to main becomes the production deployment at groundwork.austincoders.com. The project's first Vercel hostname, groundwork-ivory-beta.vercel.app, is listed in lib/site.ts as a legacy host, and next.config.ts answers any request for it with a permanent redirect to the same path on the real domain.

There is no CI service. The checks run on the author's machine, by hand: npm run check, a build and npm run test:e2e. A preview can be live before any of them has run. Vercel's commit status, called Vercel, says whether the preview built.

feature/* push once, when final merge to main push or pull request dependabot/* weekly, grouped Checks by hand check, build, e2e No automatic check main, Dependabot PRs Vercel preview about 190 MB each Production never deleted while live Daily cleanup 03:17 UTC, deletes old ones deploys deploys
Vercel deploys branches, and the author checks them by hand. The red box is the one kind of branch that is neither checked nor deployed. The yellow box deletes old previews and old production builds, never the live one.

What a deployment runs

Vercel runs npm run build, which triggers the prebuild script first. That script does two things:

  • scripts/copy-wasm-assets.mjs copies TypeScript's lib files into public/wasm/.
  • scripts/build-react-sandbox.mjs bundles the React sandbox with esbuild.

Together they come to 1.4 MB. The script can still copy Pyodide and sql.js into the build with --all, but the build does not pass that flag, so those runtimes come from jsDelivr. package.json pins Node to 24.x, the same major as the Vercel project settings, and .nvmrc says 24 for the local tools. When the two disagree, Vercel warns on every build that engines overrides the project setting.

vercel.json, in full

The whole file is one rule: "git": { "deploymentEnabled": { "dependabot/**": false } }. Dependabot opens grouped pull requests every week, one each for Next, React, CodeMirror and the dev tooling, plus monthly ones for GitHub Actions. Each would otherwise build a full preview deployment that nobody would ever open. Nothing runs on those branches either: with no CI service, a bump is checked only when someone pulls the branch and runs npm run check and a build, which answer whether it breaks anything without a deployment.

Why the storage filled up

Vercel keeps every deployment's build output until somebody deletes it. There is no automatic expiry on the Hobby plan, and the plan's deployment storage limit is 10 GB. Each deployment of this site is about 190 MB. A local production build shows where that comes from: .next/server/app, which holds the prerendered HTML and RSC payloads for every page, is 198 MB on its own. A site that prerenders everything pays for it in every deploy.

WhenWhat happened
15 SeptemberStorage reached 11.21 GB of 10 GB. A weekly cleanup workflow was added, keeping 5 production and 5 preview deployments plus anything under 24 hours old. Its first run took the count from 121 to 15.
25 SeptemberStorage was back at 8.2 GB: 43 deployments of about 190 MB each. Most were under a day old, so the 24-hour rule protected them. The cleanup became daily with tighter limits, and Dependabot branches stopped deploying.
After that cleanupThe project has 7 deployments. Usage across the whole Vercel team still showed 10.32 GB of 10 GB, because other projects count too.

.github/workflows/vercel-cleanup.yml runs at 03:17 UTC every day. The odd minute avoids the pile-up of scheduled jobs on the hour. It can also be started by hand, with a dry_run switch. It passes three secrets and three limits to scripts/vercel-cleanup.mjs, which:

  • pages through every deployment, 100 at a time
  • asks the project which deployment is live in production
  • keeps the live deployment, the 3 newest production deployments, the 2 newest previews, and anything under 6 hours old
  • deletes the rest, but only when it is given --execute

Without that flag it just lists what it would delete. The last manual run took 12 seconds.

The six-hour window is the soft spot. Every deployment younger than that is kept, however many there are. Ten pushes in one afternoon are ten deployments, close to 2 GB, until the next morning. That is why pushes are batched: one push at the end of a day's work is one deployment.

Two sets of defaults

The script's own defaults are 5 production, 5 previews and 24 hours, the original weekly rule. The workflow overrides them with 3, 2 and 6. Running npm run vercel:cleanup locally without those variables applies the older, looser rule.

Knowing when something breaks

Sentry, when it is switched on. Error tracking is wired in but inactive until NEXT_PUBLIC_SENTRY_DSN is set. Everything keys off one boolean in lib/errorTracking.ts, and every call site loads the SDK with import("@sentry/nextjs"), so without a DSN its 548 KB chunk is never fetched:

  • instrumentation.ts initialises Sentry on the server and reports request errors through Next's onRequestError hook.
  • instrumentation-client.ts initialises it in the browser and records route transitions.
  • reportError() tries Sentry first and falls back to the site's own endpoint.

The options are conservative: 10% of traces are sampled, session replay is off, personal data is off, and the environment comes from VERCEL_ENV. The DSN's host is added to the policy's connect-src at build time, since a blocked request would fail silently. Five unit tests check those options and that origin. Whether a DSN is set in production is a Vercel setting, and this repository cannot show it.

The fallback. components/ErrorReporter.tsx sits in the root layout. When Sentry is off, it listens for error and unhandledrejection on the window and posts up to 5 reports per page load to /api/client-error, using keepalive so a report survives the page closing. The route writes one JSON line to the function log. That line has no grouping, no alert and a limited lifetime, which is exactly why Sentry was added. When Sentry is on, the component does nothing and Sentry's own handlers take over.

Speed Insights and Analytics. The root layout also renders <Analytics /> and <SpeedInsights />, both from Vercel's version 2 packages. Speed Insights records Core Web Vitals from real visits, the field numbers that Lighthouse's lab numbers only predict. Analytics counts page views and nothing else: the code sends no custom events. Their scripts are served from /_vercel/ paths that exist only on Vercel, so they return 404 on a local server and the smoke tests ignore them.

What this setup does well, and what it does not

Every branch is testable at a real URL before it merges, and a broken build never reaches readers, because a failed build never replaces the live deployment. What it lacks is a gate that runs without the author: the checks are a command someone has to remember, and nothing in this repository makes Vercel wait for them before it promotes a build of main. On a one-person project that is a reasonable trade. With a second person it would be the first thing to change.