ADR-007 — Frontend SPA servido same-origin pela API (monorepo, shadcn/ui)
Date: 2026-06-12
Status: Accepted
Pareado com: ADR-006 · brainstorm docs/brainstorms/2026-06-12-frontend-monorepo-shadcn-brainstorm.md
Context
O template passou a nascer com autenticação BFF same-origin (ADR-006): o browser usa um cookie de sessão HttpOnly + X-XSRF-TOKEN, sem token em JavaScript. Esse modelo só funciona se o SPA for servido na mesma origem da API — caso contrário, o cookie vira cross-site (exigindo SameSite=None + CORS credenciado, enfraquecendo a postura CSRF).
O template precisa de um frontend de referência (paridade com o canon backend, onde Orders é referência viva) usando shadcn/ui (React + Tailwind), entregue em monorepo com o backend.
Decision
Um único repositório (o próprio ITLab.Template.Next) com o SPA servido same-origin pela API.
- Projeto:
src/ITLab.Template.Next.Spa— Vite + pnpm + React + TypeScript + Tailwind + shadcn/ui. Nome.Spa(não.Web, que é ambíguo: a API também é web). - Serving: a API serve o SPA. Produção:
UseStaticFiles()+MapFallbackToFile("index.html")(após os endpoints/v1/*,/.well-known/*,/health/*, que não são capturados pelo fallback). Desenvolvimento:Microsoft.AspNetCore.SpaProxy(ouserver.proxydo Vite) faz uma única origem frontear o dev server do Vite + a API. - Build integrada: o
Api.csprojdisparapnpm install+pnpm buildnopublish/Release, copiandodist→wwwroot. Um único artefato, um único deploy. - Sem tool de monorepo JS (Nx/Turborepo): é um único app de front — YAGNI. pnpm puro.
- API client:
openapi-typescript(tipos a partir do OpenAPI 3.1 já exposto) +openapi-fetch. Stack de dados: TanStack Query; forms: react-hook-form + zod; routing: React Router. - Canon de UI: auth (login/logout/guarda de rota/CSRF/
GET /v1/auth/me) +Orders(list/create/approve, role-gated) como referência viva.
Consequences
Positivas:
- O cookie BFF do ADR-006 funciona sem CORS nem hacks de SameSite — same-origin por construção.
- Um deploy, um artefato;
dotnet run/dotnet publishcobrem back+front. - Tipos da API ponta-a-ponta via OpenAPI; o front acompanha o contrato do backend.
Negativas:
- O build do .NET passa a depender de Node + pnpm (CI precisa do toolchain JS).
dotnet newprecisa templatizar o SPA (rename dosourceName, excluirnode_modules/dist) — trabalho da Fase 3 de empacotamento (TODO FE-09).- Acopla o ciclo de build do front ao da API (mitigado: SpaProxy só em dev; build do front só no publish/Release).
Alternatives considered
| Opção | Por que descartada |
|---|---|
| Hosting separado (SPA em CDN/static host) + reverse proxy unificando origem | Mais partes móveis (gateway, 2 deploys) para o mesmo resultado de origem; só compensa se o front precisar escalar/CDN independente. |
| Origens separadas + CORS credenciado | Quebra o BFF puro (SameSite=None, CSRF mais fraco) — contraria o ADR-006. |
| Nx/Turborepo | Pesado para um único SPA + um backend .NET; mais conceitos para o consumidor manter. |
| Cliente tipado à mão (sem codegen) | Tipos divergem da API com o tempo; o OpenAPI já existe e dá geração barata. |
References
- ADR-006 — Local Identity dual-scheme (por que same-origin).
docs/brainstorms/2026-06-12-frontend-monorepo-shadcn-brainstorm.md.docs/todos/2026-06-12-todos-frontend-spa.md(plano FE-01..FE-10).
Adendo de implementação (2026-06-12, v1.0.1 — não altera a decisão). O mecanismo de dev evoluiu em relação às opções cogitadas acima: o caminho primário é um reverse proxy do próprio host ASP.NET para o Vite dev server (Microsoft.AspNetCore.SpaServices.Extensions, ativado por Spa:DevServerUrl), navegando em https://localhost:52106 — mesma origem da produção, HMR via websocket proxiado, prefixos de API fora do proxy. O server.proxy do Vite permanece como fluxo alternativo (apontando para o endpoint HTTPS — o HTTP dispara o 307 do UseHttpsRedirection e quebra o cookie BFF). Microsoft.AspNetCore.SpaProxy foi descartado (redireciona o browser para a origem do Vite em vez de proxiar). Ver CHANGELOG.md § 1.0.1 e Program.cs § SPA serving.