This picks up exactly where How to Green-Light a Build leaves off — decision made, GO on the record. This lesson covers turning that decision into a running, tested local build: a web application with real screens, real accounts, and a working login, not a static site. "Local" here includes whatever preview URL your deploy pipeline creates as a side effect of being wired up early — the boundary this lesson respects is between running-and-tested (this lesson) and live-at-your-real-discoverable-domain (shipping, the next lesson in this series). If you don't have an approved brief, brandbook, mockup, and spec yet, stop and go get them — this lesson assumes all four exist and starts writing code on top of them, not before them.
TL;DR
Coding a web app is not "open an editor and start on the homepage." It's five ordered moves. First, stand up infrastructure — repo, database, environment variables, auth — before any screen exists, so the first deploy validates the pipeline, not a feature. Second, run a test-and-refine loop that treats a screenshot as proof of rough layout only, never proof of an exact value — verify anything specific (a font, a computed style, a database write) directly, and audit the live rendered page, not the diff, because a hardcoded placeholder number reads as real data in code review and only fails when someone looks at the actual page. Third, treat SEO, GEO, and AEO as three separate checklists that overlap but aren't the same thing — classic search, LLM-citability, and answer-engine formatting each have distinct requirements. Fourth, verify mobile correctness explicitly, with the same rigor as desktop, because "the CSS has breakpoints" is not the same claim as "someone resized a real browser to a phone width and looked." Fifth, pull every string of copy — UI labels, emails, empty states, seed content — from the brief's voice section, not from your own defaults; formality register and punctuation conventions are brief decisions, and the style document itself has to obey its own rule.
Do these five in order and you can take an approved spec to a new service running locally, exercised in a real browser, checked for SEO/GEO/AEO and mobile correctness, and consistent with the brief's voice — ready for the next lesson in this series, which covers shipping it.
Step 1 — Infrastructure before screens
The instinct is to open the editor and start on the page that feels most fun to build. Resist it. The build order that actually holds up:
1. Private repo created
2. Managed database provisioned, service wired to the repo, auto-deploy from main configured
3. Every environment variable set — before app code exists
4. Scaffold: framework init → install ORM + auth → write the schema → migrate → seed
5. First screen
The first deploy in this order validates the pipeline — repo connects, service builds, database connects, env vars resolve. If that deploy fails, you know it's infrastructure, not a feature you just wrote. Reverse the order and a broken first deploy could be either, and you burn time bisecting. This first deploy lands on the platform's auto-generated URL, not your real domain — treat it as a second local environment for now, not a launch; Step 3 covers keeping it out of search until it's actually meant to be found.
A few details worth carrying into every stack:
- Seed reference data before the first screen renders. A screen built against a real seeded row (even one row) behaves differently than one built against an empty array or a hand-typed placeholder — you catch shape mismatches immediately instead of at the point you finally wire up real data.
- Expect two database connection strings on most managed platforms. A private-network URL that only resolves inside the platform's runtime, and a public proxy URL for migrations and seeding from your own machine. Name both in an
.env.examplewith names only, never values — the next agent (or you, next session) needs to know two URLs exist before hitting a confusing connection failure. - One explicit site-URL environment variable, used for every absolute link — password-reset emails, canonical tags, Open Graph images, verification links. Never derive an absolute URL from the incoming request inside a containerized deploy; the request's host can resolve to an internal container name instead of your real domain, and you won't notice until a link in a real email points at
localhostor an internal hostname. - Decide what's a database row and what's a file, and honor that decision in the schema. If the spec already decided blog content ships as markdown files compiled at build time rather than a
Posttable (a call the spec should have made — see How to Write a Spec), don't quietly reintroduce aPostmodel out of habit halfway through scaffolding. - If the app is multi-language, define the translation keys before the first screen, not after. A language switcher drawn into the mockup is a scope commitment; retrofitting i18n keys onto screens already built with hardcoded strings is strictly more work than starting with the key structure in place, and it's the kind of scope-narrowing that quietly happens when this step gets skipped under time pressure.
Local run, concretely, is: framework dev server pointed at the public proxy database URL, migrations applied, seed script run once. If a new agent can't get from a fresh clone to a working local screen in the time it takes to read the .env.example and run three commands, the scaffolding step isn't done yet.
Step 2 — The test-and-refine loop
Once the first screens exist, the loop is: build → run in a real browser → have an agent analyze what actually rendered → fix → repeat. Two failure modes show up constantly here, and both are worth naming explicitly because they're counterintuitive.
A screenshot proves rough layout. It does not prove an exact value. A page can render in a fallback serif instead of the two webfonts a brandbook specified, and a screenshot review at a glance will pass it — a fallback serif looks close enough to an intentional serif that the eye doesn't flag it. The bug is only visible if you check the computed value directly: read the rendered font-family with a computed-style query, or measure actual text width against the expected width for the real font. (The root cause, worth knowing because it's a general trap, not a one-off: a CSS custom property aliasing a font variable declared on :root, while the font loader's own variable only gets attached to <body> — the alias can't resolve at the point it's declared, freezes as invalid, and silently inherits down the whole tree.) The rule that generalizes: when a requirement is exact — a font, a specific token value, a spacing constant — assert the computed value programmatically. Don't eyeball a screenshot for it.
Audit the live rendered page, not the code. Placeholder numbers copied from the mockup stage — fake "member count" or "listings this week" style stats — can survive all the way to a production homepage, sitting directly above a section that's honestly showing zero real activity. Code review misses this reliably, because a plausible-looking hardcoded number reads as real data; nothing about const stats = { members: 340 } looks like a bug in a diff. It only surfaces when an agent looks at the actual rendered page and asks "is this true right now?" The fix generalizes past this one example: any number, count, or "as of" claim shown to a user should be computed from the real data source, not carried over from the mockup that was built before there was real data.