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
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.<app> short name (subdomain, DB name, service name), <api-port> (unused on the server)00-setup-profile.mdPin 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".
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
| 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 |
Secure, reboot test passed (all services came back alone)README.md, DEPLOY.md, DESIGN.md, CLAUDE.md exist and are current