--- title: Default stack summary: The stack every app here starts from, the versions actually in use, and why each piece was chosen last_verified: 2026-09-26 --- # Default stack Versions are what two production apps actually ran on 2026-09-26 (`^` = caret range in `package.json`). Pin what is marked **pinned**. For the rest, stay on the major shown until you choose to move. ## App with accounts (`api/` + `web/`) ### api/ | Package | Version | Why | |---|---|---| | Node | 22 LTS on the server | run built JS: `node --max-old-space-size=128 dist/server.js` | | `fastify` | ^5.12 | fast, schema-friendly, small memory footprint | | `@fastify/cookie` | ^11.1 | session cookie | | `@fastify/rate-limit` | ^10.3 | login/reset limits (key on the real client IP header, see new-web-app.md) | | `prisma` + `@prisma/client` | **6.19.3 pinned** | migrations + typed client. Prisma 7/8 are out and the CLI nags about them on every run; ignore it until you choose to migrate on purpose | | `zod` | ^4.6 | validate every request body | | `@node-rs/argon2` | ^2.2 | password and reset-code hashing (native, no build step) | | `typescript` | **~5.9 pinned** in api/ | the Prisma 6 toolchain is proven on 5.9 | | `tsx` | dev only | local watch mode. **Never in production** | | `esbuild` (or `tsc`) | build | bundle `src/` → `dist/` for production | ### web/ | Package | Version | Why | |---|---|---| | `react`, `react-dom` | ^19.2 | | | `vite` | ^8.3 | dev server + static build | | `tailwindcss` + `@tailwindcss/vite` | ^4.3 | v4: no config file, one Vite plugin, `@import "tailwindcss"` | | `shadcn` (CLI) + `radix-ui` | ^4.21 / ^1.6 | components copied into `components/ui/`, preset **radix-nova** | | `cn` | ^0.2–0.3 | the radix-nova preset imports `cn` from this npm package (shadcn's own). It's legitimate, not a stray dependency | | `class-variance-authority`, `tw-animate-css` | ^0.7 / ^1.4 | variants and animations shadcn expects | | `@tanstack/react-query` | ^5.102 | server state, caching, retries | | `react-router-dom` | ^7.18 | routing | | `lucide-react` | ^1.45 | icons | | `sonner` | ^2.0 | toasts | | `date-fns` | ^4.4 | dates (no moment) | | `idb-keyval` | ^6.3 | small IndexedDB store for offline queues/snapshots | | `next-themes` | ^0.4 | light/dark switch when needed | | `vite-plugin-pwa` + `workbox-*` | ^1.3 / ^7.4 | installable app, precache, runtime caching | | `vitest` | ^4.1 | unit tests | | `typescript` | ~6.0 in web/ | | | `oxlint` | ^1.81 | fast lint | Optional, per product: `recharts` (charts), `fuse.js` (fuzzy search), `tesseract.js` (in-browser OCR, but self-host its worker/wasm/language files, see `workflows/pwa-and-caching.md`), `@fontsource/*` (self-hosted fonts). ### Database PostgreSQL 16+ bound to `localhost` only, one database and one role per app. Tests use a real Postgres database per test run (`_test`), or one per parallel agent. ## Static site / marketing page | Option | Use when | |---|---| | Vite + React + Tailwind v4 + shadcn + `motion` | one-page sites, portfolios, funnels; output is static files served by Nginx | | Next.js static export (`output: "export"`) + Tailwind v4 + `motion` | multi-page content sites, several languages (`/ar/…` RTL), per-page metadata | | Lenis | weighted smooth scroll for "editorial" sites; pair with `motion`'s `whileInView` | Static sites need **no Node process on the server**: build locally, rsync `dist/`/`out/`. ## Why this stack - **One language end to end** (TypeScript), so any agent can work anywhere in the repo. - **Small on the server:** built Node apps with a 128 MB heap cap sit at ~40–80 MB each, so several share a 1 GB droplet. - **Your own server, no hosted backend:** Postgres + Fastify on your box. Email is the one outsourced piece, over HTTPS (`workflows/email-sending.md`), because cloud providers commonly block SMTP ports. - **shadcn/ui is source, not a dependency:** components live in your repo and follow your tokens. ## Retrofitting Tailwind v4 into an app with hand-written CSS Tailwind v4 emits cascade layers, and **any unlayered rule beats any layered rule** whatever its specificity. So old CSS silently wins. During the move, import the old stylesheet into the lowest layer: ```css @layer legacy, theme, base, components, utilities; @import "./index.css" layer(legacy); @import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/utilities.css" layer(utilities); /* preflight left out on purpose until the end */ ``` Convert screen by screen, then delete the old file and switch to `@import "tailwindcss"`. ## Environment traps - **Node 25 ships its own `localStorage`** that shadows jsdom's in tests. Shim it in the test setup file. - jsdom has no `IntersectionObserver`; stub it for components using `whileInView`. - macOS ships **openrsync**, which has no `--chown`. Rsync to a staging dir, then let the server's rsync publish with the right owner.