kitdocs · github

New web app — the default greenfield path

raw markdown workflows/new-web-app.md verified 2026-09-26

Goal

A live app at https://<app>.<domain> 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.

<repo>/
├── 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 (<app>_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

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