--- title: New web app — the default greenfield path summary: From an idea to a live, installable, tested web app on your own server — stack, repo layout, auth, tests, CLAUDE.md, deploy, portfolio when_to_use: Starting any new app with accounts and a database (for a static marketing site, skip the api/ half and go straight to ship-a-site) prerequisites: A server set up per workflows/new-server.md; Node 22+ and Postgres on your laptop; a domain on Cloudflare; the requirements written down (by the user or client, not invented) related: [reference/stack.md, reference/CLAUDE.md.template, workflows/design-first.md, workflows/ship-a-site.md, workflows/lean-production.md, workflows/password-reset.md, workflows/email-sending.md, workflows/browser-flow-testing.md, workflows/pwa-and-caching.md, workflows/backups-and-restore.md, 00-setup-profile.md] last_verified: 2026-09-26 --- # New web app — the default greenfield path ## Goal A live app at `https://.` with login, a database, unit tests, browser flows, a one-command deploy that builds on your laptop, a nightly backup, and a `CLAUDE.md` that lets any AI agent work on it safely. ``` / ├── api/ Fastify + Prisma + zod → built to api/dist, run with node │ ├── prisma/schema.prisma │ ├── src/{app.ts,server.ts,routes/,email/} │ └── test/ real Postgres test DB (_test) ├── web/ React + Vite + Tailwind v4 + shadcn/ui → static files │ ├── src/{pages/,components/ui/,lib/} │ └── public/ ├── scripts/ │ ├── deploy.sh build locally → rsync → migrate → restart → health check │ └── browser-flows/ headless Chrome flows (cdp.mjs + one file per journey) ├── DESIGN.md the design system (tokens, components, rules) ├── CLAUDE.md rules for AI agents (from reference/CLAUDE.md.template) ├── README.md what it is, how it fits together, local dev └── DEPLOY.md how it ships, where things live on the server ``` ## Inputs - Requirements: who uses it, where, and the must-have features. Write them in `REQUIREMENTS.md`. **Don't add features nobody asked for.** For a client pitch, a small demo that is right beats a larger one full of guesses. - `` short name (subdomain, DB name, service name), `` (unused on the server) - Setup choices from `00-setup-profile.md` ## Steps 1. **Pin the stack** (`reference/stack.md`) and write it into `CLAUDE.md` as locked: React 19 + Vite + Tailwind v4 + shadcn/ui + TanStack Query in `web/`; Fastify 5 + Prisma 6 (pinned) + zod + argon2 in `api/`; Postgres. Don't switch frameworks mid-product. **Check:** `CLAUDE.md` names the versions and says "don't upgrade majors without asking". 2. **Design before code** for anything with more than two screens (`workflows/design-first.md`). Save `DESIGN.md` in the repo. **Check:** every planned screen has an artboard or a line in `DESIGN.md`. 3. **Model the data so history outlives change.** Keep records (what happened) apart from plans/config (what should happen), so editing a plan never rewrites history. Store canonical units (kg, UTC timestamps, integer minor currency units) and convert only at the screen. **Check:** deleting or editing a plan cannot change a past record. 4. **Auth:** email + password hashed with argon2, session cookie `HttpOnly; Secure; SameSite=Lax`, sessions that renew past halfway, login rate-limited per real client IP (behind Cloudflare, key on the header your proxy sets from `CF-Connecting-IP`, never the left-most `X-Forwarded-For`). Add a reset flow early (`workflows/password-reset.md`). Decide whether signup is open or invite-only; ask the owner. **Check:** the cookie is `Secure` on the real domain; the 11th login attempt in a minute gets 429. 5. **API rules that prevent real incidents:** - `Cache-Control: no-store` on **every** API response (an onSend hook). A CDN will otherwise cache URLs ending in `.csv`/`.json` and serve one user's data to anyone. - Build one DTO function per entity (e.g. `userDto`) and use it everywhere, so the login response and `/me` can't drift apart. - Take "today" from the client (`toLocaleDateString("en-CA")`), never the server's UTC date. Otherwise users east of UTC get yesterday after midnight. - A wrong password on a destructive action → **403**, not 401, if your client logs out on any 401. **Check:** `curl -I` any API URL without cookies shows `no-store`; a test asserts login body equals `/me`. 6. **Tests from day one:** API tests against a real Postgres test DB (not mocks), web unit tests for the pure logic, and **browser flows** for every user journey (`workflows/browser-flow-testing.md`). A flow compares the DOM with the API after each action, then reloads and compares again. **Check:** `npm run typecheck && npm test` pass, and each journey has a flow that passes at 390 px. 7. **Make it installable if it's used on phones** (`workflows/pwa-and-caching.md`): manifest, icons, service worker, and self-hosted heavy assets (no CDN fetches inside workers). **Check:** it installs on a phone, and a reload after a deploy shows the new build. 8. **Write `CLAUDE.md`** from `reference/CLAUDE.md.template`: stack lock, commands, the test cycle after each change, the deploy rule, branch rules, test account (throwaway, `@example.com`), privacy rule. **Check:** a fresh agent session can run tests and a flow using only `CLAUDE.md` and `README.md`. 9. **Ship it** (`workflows/ship-a-site.md`, `workflows/lean-production.md`): DNS record (proxied), Nginx server block, TLS, a Postgres role + DB, a systemd unit running `node --max-old-space-size=128 dist/server.js` with `MemoryMax=`, and `scripts/deploy.sh` that **builds on your laptop**, rsyncs, migrates, restarts and health-checks, failing closed. **Check:** the deploy prints the deployed commit, `/api/health` → 200, and the service's memory is measured right after the deploy and again a day later. 10. **Backups and restore** (`workflows/backups-and-restore.md`): nightly dump, off-machine copy, a restore you have actually run. **Check:** the new DB appears in the restore drill. 11. **Add it to your portfolio / landing page** with an honest status line ("a pitch", "in progress", "live"). Don't upgrade the wording to sound better. **Check:** the page links the live URL and shows a current screenshot (capture with the service worker unregistered, or you get the old build). ## Traps | Symptom | Cause | Fix | |---|---|---| | One user's export served to strangers | CDN caches any URL with a file extension by default | `no-store` on every API response, tested | | Server runs out of memory during deploy | `npm install`/build on a 1 GB server | build on the laptop, ship `dist/` | | App uses 300+ MB at idle | running TypeScript through a dev runner (`tsx`, `ts-node`) in production | bundle to `dist/`, run `node` with a heap cap | | Data "from yesterday" after midnight | server-side "today" in UTC | client sends its local date | | After login, UI shows the previous user's settings | global query cache cleared in a way that detaches long-lived observers | a `switchUser()` that resets per-user state explicitly | | Deploy dies at the migrate step | a `.env` value with `<`, spaces or quotes sourced by bash unquoted | quote every such value | | Feature built that the owner never wanted | requirement invented | ask; build only what was given | ## Done when - [ ] Live on HTTPS, cookie `Secure`, reboot test passed (all services came back alone) - [ ] Typecheck, unit tests and every browser flow pass locally and against production (test account) - [ ] Deploy builds on the laptop; RAM measured after deploy and a day later - [ ] Nightly backup + a restore actually run - [ ] `README.md`, `DEPLOY.md`, `DESIGN.md`, `CLAUDE.md` exist and are current - [ ] Listed on the portfolio page with an honest status