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.
api/ + web/)| 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 |
| 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).
PostgreSQL 16+ bound to localhost only, one database and one role per app. Tests
use a real Postgres database per test run (<app>_test), or one per parallel agent.
| 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/.
workflows/email-sending.md), because cloud
providers commonly block SMTP ports.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:
@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".
localStorage that shadows jsdom's in tests. Shim it in the
test setup file.IntersectionObserver; stub it for components using whileInView.--chown. Rsync to a staging dir, then let
the server's rsync publish with the right owner.