# APP-STACK — the stack and rules for turning an approved prototype into a real running app

> Section numbers in this file (`## 1`, `## 2`, ...) are cited from other documents as "APP-STACK.md section N" —
> **never renumber an existing section.** Add new material as a new section at the end, or as a lettered
> sub-section (`4b`), the same convention `DESIGN-SYSTEM.md` uses.

---

## 1. What this document is for

`PROJECT-STRUCTURE.md` section 4 defines the file layout of a "real system." This document defines the
concrete stack that fills that layout for the first hop past a prototype: **the business owner runs the app
on their own machine, across several browsers, with real stored data and a real login** — not yet a live
multi-user production deployment.

The goal of fixing the stack here is stated plainly: a developer who later takes this project over per
`HANDOFF-PROTOCOL.md` never has to rewrite it. It is already the same stack the team uses everywhere else —
same framework versions, same folder shape, same rules. Picking it up means reading it, not redoing it.

This is a decision already made — the stack below is fixed, not a menu of options. Do not substitute
alternatives (a different ORM, a different query library, a different CSS approach) without updating this
file first, for everyone.

---

## 2. The stack

### Frontend

| Layer | Choice |
|---|---|
| UI library | React 19 |
| Build tool | Vite 7 |
| Language | TypeScript 5.9, strict mode |
| Styling | Tailwind 4 |
| Server state / caching | TanStack Query 5 |
| Routing | React Router 7 |
| Forms | react-hook-form, with Zod 4 resolvers |
| Icons | lucide-react |
| Toasts | sonner |

### Backend

| Layer | Choice |
|---|---|
| HTTP server | Fastify 5 |
| ORM | Prisma 7 |
| Database | SQLite, a single file |
| Input validation | Zod 4 on every input, no exception |
| Password hashing | bcrypt |
| Sessions | `@fastify/cookie` |

### Fonts

No web font is loaded here either — the system font stack defined in `DESIGN-SYSTEM.md` section 1 applies
unchanged. Do not add a `<link>` to Google Fonts or bundle a font file.

---

## 3. Why SQLite

SQLite is a deliberate choice, not a shortcut:

- **It is one file.** Backing it up is copying a file; moving the app to another machine is copying a file.
  Nothing to install, no Docker, no separate database server to keep running.
- **Prisma sits in front of it.** Because all queries go through Prisma's query API rather than hand-written
  SQL, a developer can point the same schema at PostgreSQL later — changing one connection string and the
  `provider` line in `prisma/schema.prisma` — without rewriting query code.

**The team's production systems run PostgreSQL.** This app's SQLite file is the on-ramp, not the destination.
The migration path from here to Postgres is the intended path, not a fallback in case something breaks.

---

## 4. File layout

This extends `PROJECT-STRUCTURE.md` section 4 with the concrete detail this stack needs. Everything from
section 4 still applies (naming per section 5, split-by-unit-of-change per section 6, cleanup per section 7);
this section only fills in what section 4 leaves generic.

```
{project-name}/
├── README.md  AGENTS.md  CLAUDE.md  .gitignore  .env.example  CHANGELOG.md
├── docs/
│   ├── SPEC.md  RULES.md  ARCHITECTURE.md  handoff/
├── src/
│   ├── modules/{topic}/          <- feature code, grouped by subject
│   │   └── orders/
│   │       ├── ui/               <- screens and components used only by this module
│   │       ├── logic/
│   │       └── types.ts
│   ├── shared/                   <- only things used by 2+ modules
│   │   ├── ui/                   <- shared components: dialogs, tables, toasts wiring
│   │   ├── lib/
│   │   └── types/
│   ├── router.tsx                <- React Router route tree
│   └── config/
│       └── env.ts                <- the ONLY file that reads import.meta.env
├── server/
│   ├── routes/                   <- 1 file = 1 endpoint group, validates input, calls services/
│   ├── services/                 <- business logic lives here, never in routes/
│   ├── db/
│   │   └── client.ts             <- the ONLY file that reads process.env for DB config
│   ├── middleware/                <- auth check, rate limit, error handler
│   └── env.ts                    <- the ONLY file that reads process.env on the server
├── prisma/
│   ├── schema.prisma              <- table structure, single source of truth for the data shape
│   ├── migrations/                <- ordered by time, never edit one that has already run
│   └── dev.sqlite                 <- the actual database file — gitignored, never committed
├── tests/
│   ├── unit/  integration/  e2e/
├── scripts/                       <- backup, seed, deploy
└── .archive/
```

Notes:

- **The SQLite file lives at `prisma/dev.sqlite`** (or a name of the project's choosing, kept consistent),
  and `.gitignore` must list it explicitly — a database file with real entered data must never enter git
  history. Only `prisma/schema.prisma` and `prisma/migrations/` are committed.
- **Env is read in exactly one place per side** — `src/config/env.ts` on the frontend, `server/env.ts` on the
  backend. Nothing else touches `import.meta.env` or `process.env` directly. This is stricter than "read env
  in `config/`" from `PROJECT-STRUCTURE.md` — a backend has request-time secrets a frontend must never see,
  so the two sides get two separate single-reader files, not one shared one.
- **`server/db/client.ts`** is the one file that constructs the Prisma client; everything else imports the
  client from there, never instantiates its own.

### The one-instruction-one-file test

`PROJECT-STRUCTURE.md` section 6 asks: how many files does one typical edit request have to touch? The same
test applies here. Two edits are used as the standing check for this layout:

| Request | Should touch |
|---|---|
| Change the app's name (shown in the navbar, page title, emails) | 1 file — a single constant/config value, never hardcoded per-screen |
| Change the primary color | 1 file — the Tailwind theme token, never a hex value repeated across components |

If either of these touches more than one file, the layout has drifted and needs fixing before more feature
work is added on top of it.

---

## 5. Standing up the project

```bash
# frontend
npm create vite@latest {project-name} -- --template react-ts
cd {project-name}
npm install @tanstack/react-query react-router-dom react-hook-form @hookform/resolvers zod \
  lucide-react sonner
npm install -D tailwindcss @tailwindcss/vite

# backend, inside the same project (server/ folder)
npm install fastify @fastify/cookie zod bcrypt
npm install -D prisma
npx prisma init --datasource-provider sqlite
```

Then wire Tailwind 4 via its Vite plugin (`@tailwindcss/vite`) rather than a PostCSS config — Tailwind 4's
supported path with Vite.

### Required `package.json` scripts

| Script | Does |
|---|---|
| `dev` | Runs the Vite dev server and the Fastify server together (concurrently, or via a single dev-orchestrator script) |
| `build` | Type-checks then builds the frontend for production |
| `typecheck` | `tsc --noEmit`, must pass clean before anything is called done |
| `lint` | Runs the project's linter, zero errors before anything is called done |
| `db:push` | `prisma db push` — applies the current schema to the SQLite file without a migration file, for fast local iteration |
| `db:seed` | Runs a seed script that populates the SQLite file with sample data, so the owner opens the app to something, not an empty shell |

`db:push` is for iteration; once the schema stabilizes, real `prisma migrate dev` migrations replace ad-hoc
`db:push` calls so there is a migration history a developer can read later.

---

## 6. Code rules

These are the team's existing standards, carried over unchanged:

- Max 250 lines per file — split once a file grows past that.
- Never `any` — use `unknown` and narrow it.
- No `console.log` — `console.debug` / `console.error` / `console.warn` only.
- TypeScript strict mode; `tsc --noEmit` must pass before anything is called done.
- Named exports only.
- Zod validates every API input, without exception.
- ESM `import`, with `.js` extensions on relative imports.
- Gate before done: type check, lint, and build must all pass.
- Routes contain no business logic — they validate input and call a service in `server/services/`.
- Environment variables are read in one place only per side (section 4) — nothing else touches
  `process.env` or `import.meta.env`.

---

## 7. Security baseline

All of the following must be present — none are optional at this stage:

| Requirement | Detail |
|---|---|
| Password hashing | bcrypt; passwords are never stored or logged in plain text, anywhere, including debug output |
| Session cookie | `httpOnly`, `sameSite`, and `secure` when served over https |
| Login rate limiting | Repeated failed logins from the same source are throttled |
| Server-side authorization | Every request checks who is asking and what they're allowed to do — the client is never trusted to enforce this |
| No secrets in code | `.env` is gitignored; `.env.example` sits beside it listing every needed variable with no real values |
| No personal data or tokens in URLs | Per `DESIGN-SYSTEM.md` section 11c — URLs persist in browser history and server logs |
| SQL only through Prisma | Never string-concatenated SQL, anywhere |

### State the limit honestly

**This app, at this stage, runs on one person's machine for their own testing.** It is not ready for real
customers or real customer data. Specifically missing, and known to be missing:

- No HTTPS — the connection is not encrypted.
- No backups — the SQLite file has no automated backup schedule.
- No monitoring — nothing alerts if the app goes down or errors spike.
- No audit trail — actions are not logged for later review.
- Single trusted user — the security model assumes the person running it is the only person with access to
  the machine; it has not been hardened against a malicious co-user of the same computer.

**It must not be pointed at real customer data, and must not be exposed to the internet, until a developer
has run `/security-review` on it.** That review is what closes this gap — this document only states honestly
that the gap exists at this stage, it does not close it.

---

## 8. How the UI rules carry over

The React app implements the same `DESIGN-SYSTEM.md` rules the prototype did. Nothing here is a new rule —
this section only maps which library in this stack provides each one, so nobody hand-rebuilds something the
stack already gives for free.

| Rule | `DESIGN-SYSTEM.md` section | Provided by |
|---|---|---|
| Fixed-frame scroll (header/footer fixed, only content scrolls) | 12b | Plain CSS/flex layout in the root app shell component — no library needed |
| Dialog sizes, three-region dialog (header / scrolling body / fixed footer) | 12c | Radix-based dialog primitives (e.g. via shadcn/ui, which wraps Radix) |
| Confirm tiers, including type-to-confirm | 12d | Built on the same dialog primitive above — no separate library |
| Toast behavior, undo-over-confirm | 12e | sonner |
| Loading states, skeletons, the 500ms first-open floor | 13 | TanStack Query's `isLoading` / `isFetching` states drive which of the section 13 states renders; the 500ms floor is app code, not a library feature |
| Four-band table screen | 9 | Plain components using TanStack Query for data + loading state; no table library is mandated |
| No emoji, sharp corners | 1b, 3 | Tailwind config (`borderRadius` capped at `sm`), enforced by review, not a library |
| `data-testid` on every interactive element | 11c | App code — set explicitly on every button, input, link, row |
| State in the URL | 11c | React Router 7's search params APIs |

Do not restate the rules themselves here — read the cited `DESIGN-SYSTEM.md` section for the actual rule.
This table only answers "which package do I reach for."

---

## 9. Checklist before calling the app real-ready

- [ ] `tsc --noEmit` passes with no errors
- [ ] Lint passes with no errors
- [ ] `npm run build` succeeds
- [ ] Every API route validates its input with Zod
- [ ] No route contains business logic — logic lives in `server/services/`
- [ ] `process.env` / `import.meta.env` are read only in the two designated files (section 4)
- [ ] `.env` is gitignored; `.env.example` exists with no real values
- [ ] `prisma/dev.sqlite` (or the project's DB file) is gitignored
- [ ] Passwords are hashed with bcrypt, never logged
- [ ] Session cookie sets `httpOnly` and `sameSite`, and `secure` when served over https
- [ ] Login is rate-limited
- [ ] Every server route checks authorization, not just authentication
- [ ] Every credential handed to a person has been used to log in against the running app first, and a wrong
      password was confirmed to be refused — an untested credential is a guess, not a credential
- [ ] The UI rules in section 8 have been visually verified per `DESIGN-SYSTEM.md` section 14
- [ ] The security limits in section 7 have been communicated to the owner in plain terms — this is a
      single-user local app, not a production system, until `/security-review` has run
