Greenfield starter - the Layer-2 paved road, runnable
A self-contained monorepo that boots on a fresh clone. It implements the stack's decisions (../DECISIONS.md) as a working app - and doubles as the reference copy of every merge-class config (../stack.manifest.json) - no placeholders, no "wire this up yourself".
nvm use # Node 24 (.nvmrc)
pnpm install
pnpm dev # creates the auth schema, then boots web + api via turbo
Open <http://localhost:3000>, create an account, land on the protected dashboard.
What you get
| Piece | Where | The decision behind it |
|---|---|---|
Next.js 16 App Router, React 19, typed next.config.ts with security headers, output: "standalone" | apps/web | DECISIONS #7 |
| Fastify 5, native plugin DI (no container), Zod-validated env at boot | apps/api | DECISIONS #5, #6 |
| Better Auth - email + password, DB-backed revocable sessions | packages/auth | DECISIONS #10 |
Next 16 proxy.ts session gate (Node runtime, queries the session DB) + /api/* rewrites to Fastify | apps/web/src/proxy.ts, next.config.ts | DECISIONS #10 |
| Default-deny at both gates - the proxy redirects, the service 401s; new routes are protected until allow-listed | apps/web/src/lib/routes.ts, apps/api/src/middleware/session-gate.ts | DECISIONS #10 |
| CSS Modules + SCSS, three-tier tokens (primitive -> semantic -> component) | apps/web/src/app/globals.scss + *.module.scss | DECISIONS #10, #3 |
Vitest unit tests co-located, Playwright e2e in e2e/, both run from the root | pnpm test:unit, pnpm test:e2e | DECISIONS #9 |
| Lighthouse perf budgets - local, report-only, committed baseline + history (no CI gate, no external upload) | pnpm test:perf, perf/ | DECISIONS #9 |
| pnpm + Turborepo + Biome (+ Prettier for SCSS), 7-day supply-chain cooldown | root configs | DECISIONS #1-3, #8, #11 |
How a request flows
browser ── :3000 ──> Next.js
├─ proxy.ts session gate (default-deny, DB-backed)
├─ /api/auth/* Better Auth route handler (in-app)
├─ /api/* rewrite ──> Fastify :4000 ── session gate again
└─ pages /, /sign-in, /sign-up public; /dashboard gated
One session cookie, validated at both gates against the same database, because both sides import the same @starter/auth instance.
Commands
pnpm dev # db:init + web (:3000) + api (:4000), watch mode
pnpm build # turbo build (next build + service typecheck)
pnpm test:unit # Vitest, co-located *.test.ts
pnpm test:e2e # Playwright journeys (boots the dev servers itself)
# first time: pnpm --filter e2e exec playwright install chromium
pnpm test:perf # Lighthouse perf budget (local, report-only) vs committed baseline
# dev/prod: PERF_TARGET=prod PERF_WEBAPP_URL=https://... pnpm test:perf:remote
pnpm check:all # format + types + lint, turbo-cached
pnpm lint # biome check .
SQLite now, Postgres in production
The starter runs Better Auth on SQLite via node:sqlite (file: data/starter.db, gitignored) so pnpm i && pnpm dev needs zero infrastructure - no Docker, no native build step. That is a bootstrapping choice, not the production shape:
- Swap: pass a
pgPool asdatabaseinpackages/auth/src/index.tsand setBETTER_AUTH_SECRET.pnpm db:initmigrates whatever the adapter points at. - The Docker test-stack (
docker-compose.test.yml, Postgres + Redis on non-default ports) is already here for that development:pnpm bootstrap:test-stack.
Layout
apps/web Next.js app (proxy gate, auth pages, dashboard, rewrites)
apps/api Fastify service (Zod env, session gate, /api/health, /api/me)
packages/auth the shared Better Auth instance + db:init migration script
e2e Playwright journeys (config at the root)
perf local Lighthouse perf-budget runner (baseline + history committed)
data/ local SQLite (created on first boot, gitignored)
Deviate from a pick? Record a superseding ADR in your repo (ADR-004 pattern) - the why for every pick is in ../DECISIONS.md.
