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.
05On this page
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.
What a deployment runs
Vercel runs npm run build, which triggers the prebuild script first.
That script does two things:
scripts/copy-wasm-assets.mjscopies TypeScript's lib files intopublic/wasm/.scripts/build-react-sandbox.mjsbundles 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.
| When | What happened |
|---|---|
| 15 September | Storage 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 September | Storage 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 cleanup | The 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.
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.tsinitialises Sentry on the server and reports request errors through Next'sonRequestErrorhook.instrumentation-client.tsinitialises 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.
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.
Finished reading?
0 of 26 chapters read in this series