# Frontend — Next.js 16 + React 19 + Tailwind 4 > Canonical frontend boilerplate for André Bassi's projects. App Router only, pnpm only, vitest only. Point an LLM here and it scaffolds/extends the frontend exactly in this pattern. ## Hard Rules 1. Next.js 16.x, App Router. Middleware file is `proxy.ts`, exported function `proxy()` (renamed from middleware in 16.0). 2. Package manager is pnpm. Never npm, never yarn. 3. Styling: Tailwind CSS 4.x via `@tailwindcss/postcss`. No CSS-in-JS libs. 4. All backend calls through `apiFetch()` from `lib/api.ts`. NEVER raw `fetch()` to internal API. 5. Internal API routes versioned under `/v1`. 6. Code identifiers English-only; user-facing UI strings pt-BR (or localized pt/es/en). 7. Tests: vitest + @testing-library/react + jsdom. eslint 9 + eslint-config-next. 8. `NEXT_PUBLIC_*` are build-time — fixed as Docker build args in fly.toml; changing them requires rebuild. ## Canonical Layout frontend/src/ ├── app/ │ ├── (auth)/ # route group: public auth pages │ │ ├── layout.tsx # server component — redirects logged-in users │ │ ├── login/page.tsx │ │ ├── register/page.tsx │ │ └── update-password/page.tsx │ ├── (dashboard)/ # route group: protected pages │ │ ├── layout.tsx # force-dynamic (per-user data) │ │ └── /page.tsx │ ├── api/ # route handlers │ │ ├── auth/callback/route.ts # OAuth callback (Supabase) │ │ └── billing/webhook/route.ts # Stripe webhook │ ├── layout.tsx # root layout │ ├── providers.tsx # context providers (Supabase, theme) │ ├── globals.css # Tailwind directives │ └── proxy.ts # middleware (Next 16 naming) ├── components/ │ ├── / # per-feature folders │ │ └── __tests__/ # colocated tests │ └── ui/ # Radix UI + custom styled primitives ├── hooks/ # useAuth.ts, useXxx.ts ├── lib/ │ ├── api.ts # apiFetch(): JWT + error handling │ ├── supabase/ │ │ ├── client.ts # browser client │ │ ├── server.ts # server client (route handlers) │ │ └── middleware.ts # auth middleware helper │ ├── format.ts # formatting helpers │ └── emails.ts # email template refs (if Resend) ├── test/ # vitest setup + utilities └── types/ # entities, API response types ## apiFetch Contract (lib/api.ts) - Attaches Supabase JWT to every request. - On 401/403: auto `signOut()` + redirect `/login?reason=session_expired` + localized toast. - Central error handling — components never parse raw HTTP errors. - SSE/EventSource exception: no header support → token via `?token=` query param. ## Critical Gotchas (learned in production — do not violate) - OAuth callback: create the `response` object BEFORE instantiating the Supabase client; use `response.cookies.set()` or Set-Cookie never reaches the browser. - Logout: call `router.refresh()` BEFORE `router.push()` to invalidate RSC cache. - Per-user pages/layouts (dashboard, subscription): `export const dynamic = 'force-dynamic'`. - Behind reverse proxy (Fly.io/Cloudflare/nginx): handle `x-forwarded-host` in route handlers. - Portals: modals inside Framer Motion `motion.div` need `createPortal(..., document.body)` to escape transform stacking context. - Next 16 breaking changes: read `node_modules/next/dist/docs/` before writing UI code on a fresh major. ## Auth Flow (Supabase) - JWT-based via Supabase Auth; custom SMTP relay when project sends own emails. - `(auth)/layout.tsx` is a server component that redirects already-logged-in users to dashboard. - `proxy.ts` (middleware) refreshes the session via `lib/supabase/middleware.ts`. ## Styling - Tailwind 4.x utilities; design tokens via CSS vars in `globals.css`. - Components in `components/ui/` wrap Radix primitives with project styling. - Responsive first: test 360px, 768px, 1280px. ## Testing pnpm vitest run # all pnpm vitest run src/components/devices # by path pnpm vitest run src/components/devices/__tests__/x.test.tsx pnpm vitest run -t "edit name" # by name - Colocate tests in `__tests__/` next to components. - jsdom environment; setup in `src/test/`. ## Build & Deploy - `pnpm build` locally; production via Docker multi-stage on Fly.io (see llms-devops.txt). - fly.toml carries `NEXT_PUBLIC_*` as `[build.args]`. - Static landing pages (no auth): Cloudflare Pages instead of Fly. ## package.json Baseline { "dependencies": { "next": "16.x", "react": "19.x", "react-dom": "19.x", "@supabase/ssr": "latest", "@supabase/supabase-js": "latest" }, "devDependencies": { "tailwindcss": "^4", "@tailwindcss/postcss": "^4", "vitest": "^4", "@testing-library/react": "^16", "jsdom": "latest", "eslint": "^9", "eslint-config-next": "16.x", "typescript": "^5" } }