ADR-802: NextJS Portal Architecture
Status: Accepted
Deployment MIDDAG: Este ADR documenta a arquitetura de deployment específica para app.middag.io (OpenNext + Cloudflare Workers). Para uso como produto de mercado, qualquer frontend headless que consuma a REST API v1 com JWT RS256 é suportado — Next.js, Nuxt, SvelteKit, ou mesmo WooCommerce como frontend nativo.
Contexto
middag-account expõe REST API v1 com JWT RS256 (ADR-701). O portal do cliente (app.middag.io) é um frontend headless separado que consome essa API. O portal atual (repo middag-app) funciona mas tem estrutura herdada do tema v4 — módulos ausentes (entitlements, contracts, environments, service-requests, quotes, licenses, downloads, documents) e organização de código inconsistente. Pode ser refatorado sem retrocompatibilidade.
Decisão
Stack
| Aspecto | Escolha |
|---|---|
| Framework | Next.js 16 + React 19 + TypeScript 6 |
| Auth | NextAuth v4 (JWT strategy, CredentialsProvider + Google + Azure AD) |
| Styling | Tailwind CSS v4 + Radix UI primitives |
| Data | TanStack React Query v5 + Axios |
| Forms | React Hook Form + Zod validation |
| Tables | TanStack React Table v8 |
| i18n | i18next + react-i18next + next-i18next |
| Deployment | OpenNext + Cloudflare Workers |
| Build | Turbopack (dev), Next.js default (prod) |
Deployment: OpenNext + Cloudflare Workers
O portal roda em Cloudflare Workers via @opennextjs/cloudflare. Configuração:
output: 'standalone'nonext.config.mjsopen-next.config.ts: R2 incremental cache (r2-incremental-cache)wrangler.toml:- Worker name:
middag-app - Compatibility flags:
nodejs_compat,global_fetch_strictly_public - D1 database:
middag-app-db(bindingDB) - R2 buckets:
middag-plugins(bindingSTORAGE) +middag-app-opennext-cache(bindingNEXT_INC_CACHE_R2_BUCKET) - Self-reference service binding:
WORKER_SELF_REFERENCE(OpenNext caching) - Observability enabled
- Worker name:
App Router + Route Groups
app/
├── (auth)/ # Rotas públicas de autenticação
│ ├── signin/ # Login (email/senha)
│ ├── signup/ # Registro
│ ├── login/ # Alias signin (redirect)
│ ├── verify-email/ # Verificação de email
│ ├── reset-password/ # Recuperação de senha
│ └── change-password/ # Alteração de senha
│
├── (portal)/ # Rotas autenticadas do cliente
│ ├── dashboard/ # Visão geral (widgets cross-domain)
│ ├── entitlements/ # Lista + detalhe de entitlements
│ ├── quotes/ # Orçamentos (aceitar, rejeitar)
│ ├── orders/ # Pedidos (lista, detalhe, cancelar)
│ ├── invoices/ # Faturas Stripe + NFSe (PDF download)
│ ├── licenses/ # Licenças (ativar, desativar)
│ ├── downloads/ # Downloads de plugins (signed URLs)
│ ├── contracts/ # Contratos (lista, detalhe, PDF)
│ ├── documents/ # Documentos (lista, download)
│ ├── environments/ # Ambientes gerenciados
│ ├── services/ # Projetos e serviços contínuos
│ ├── service-requests/ # SRs (lista, detalhe, criar)
│ ├── support/ # Tickets (integração Jira/Chatwoot)
│ ├── account/ # Perfil, equipe, colaboradores
│ └── checkout/ # Fluxo de pagamento
│
├── api/ # Route Handlers
│ ├── auth/[...nextauth]/ # NextAuth (credentials, Google, Azure AD)
│ ├── account/ # BFF account operations
│ ├── services/ # BFF service operations
│ ├── transactions/ # BFF transaction operations
│ ├── redirect/ # URL redirects
│ └── widget-chat/ # Chatwoot widget config
│
├── error/ # Error boundary page
├── layout.tsx # Root layout (providers, theme)
└── page.tsx # Root redirect → dashboardEstado atual vs target: O grupo (middag) existente será renomeado para (portal). Módulos ausentes (entitlements, quotes, contracts, documents, environments, service-requests, licenses, downloads) serão criados.
Estrutura lib/
lib/
├── api.ts # apiFetch() — universal fetch with base URL
├── cloudflare.ts # Cloudflare Workers helpers
├── r2.ts # R2 storage helpers
├── storage.ts # Client-side storage helpers
├── helpers.ts # Generic utility functions
├── dom.ts # DOM manipulation helpers
├── recaptcha.ts # reCAPTCHA integration
├── utils.ts # Shared utilities (cn(), etc.)
├── auth/ # NextAuth config, token refresh
├── constants/ # App-wide constants
├── services/ # API service modules por domínio
│ ├── base/ # Base API client (server/client)
│ ├── authService.ts # Auth API calls
│ ├── authUtils.ts # Token normalization
│ └── {domain}Service.ts # Per-domain API service
├── types/ # TypeScript types
│ ├── api/ # DTOs espelhando REST API v1
│ └── ...
└── utils/ # Utility modules (chatwoot, etc.)components/
components/
├── ui/ # Primitivos Radix UI + Tailwind (shadcn/ui pattern)
├── layouts/ # Layout wrappers (sidebar, topbar, etc.)
├── forms/ # Form components (inputs, selects, etc.)
└── {domain}/ # Domain-specific componentsAPI Client Pattern
Serviços em lib/services/ encapsulam chamadas à REST API v1. Base client injeta headers automaticamente:
| Header | Fonte | Obrigatório |
|---|---|---|
Authorization | Bearer {access_token} da session NextAuth | Sim |
X-Middag-Organization | org do JWT payload (via session) | Sim * |
X-Middag-Company | Contexto da session (middag_br / middag_global) | Não |
* Em endpoints que operam sobre dados de organization.
Token refresh automático: callback jwt do NextAuth verifica expires_at e chama /auth/refresh. Se falhar, seta error: 'RefreshAccessTokenError' → client-side redirect para login.
Authentication
NextAuth v4 com 3 providers:
| Provider | Tipo | Callback URL |
|---|---|---|
| Credentials | Email/senha | N/A (server-side) |
| OAuth 2.0 | https://app.middag.io/api/auth/callback/google | |
| Azure AD | OAuth 2.0 | https://app.middag.io/api/auth/callback/azure-ad |
Session strategy: JWT (cookie next-auth.session-token, HttpOnly, Secure, SameSite=lax, maxAge 24h).
OAuth flow: Provider → callback → POST /middag-account/v1/auth/register-email → WordPress cria/vincula user → emite JWT → NextAuth armazena tokens na session.
Detalhes completos do fluxo de autenticação em REF-701-02 §13-16.
Portal Modules → API Mapping
15 módulos mapeados para endpoints da REST API v1 (REF-801-01 §7):
| Portal Module | API Prefix | Key Operations |
|---|---|---|
| Dashboard | Multiple (aggregation) | Read widgets data |
| Quotes | /quotes/* | List, detail, accept, reject |
| Orders | /orders/* | List, detail, cancel, refund |
| Invoices | /invoices/* + /tax-invoices/* | List, detail, PDF download |
| Licenses | /licenses/* | List, detail, activate, deactivate |
| Downloads | /downloads/* | List, signed URL download |
| Contracts | /contracts/* | List, detail, PDF download |
| Documents | /documents/* | List, detail, file download |
| Entitlements | /entitlements/* | List, detail, linked entities |
| Services | /services/* | List, detail, linked SRs |
| Service Requests | /service-requests/* | List, detail, create |
| Support | (Jira API integration) | Ticket list, create |
| Teams | /organizations/* + /collaborators/* | CRUD, invites |
| Account | /auth/user, /auth/update-profile | Profile management |
| Checkout | /orders/*, /quotes/* | Payment flow |
Dual-entity filtering via X-Middag-Company header em todos os módulos.
Variáveis de Ambiente
NextAuth:
| Variável | Descrição |
|---|---|
NEXTAUTH_SECRET | Secret para JWT do NextAuth |
NEXTAUTH_URL | URL base do portal (produção) |
NEXTAUTH_DEBUG | true para debug mode (dev only) |
API:
| Variável | Descrição |
|---|---|
NEXT_PUBLIC_API_URL | Base URL do WP backend REST API |
NEXT_PUBLIC_BASE_PATH | Base path do portal (se subpath) |
OAuth Providers:
| Variável | Descrição |
|---|---|
GOOGLE_CLIENT_ID | Google OAuth Client ID |
GOOGLE_CLIENT_SECRET | Google OAuth Client Secret |
AZURE_AD_CLIENT_ID | Azure AD Client ID |
AZURE_AD_CLIENT_SECRET | Azure AD Client Secret |
AZURE_AD_TENANT_ID | Azure AD Tenant ID |
Cloudflare (wrangler.toml, não .env):
| Binding / Config | Recurso |
|---|---|
DB (D1) | middag-app-db — edge database |
STORAGE (R2) | middag-plugins — object storage |
NEXT_INC_CACHE_R2_BUCKET | middag-app-opennext-cache — ISR |
WORKER_SELF_REFERENCE | Self-binding (OpenNext caching) |
account_id | Cloudflare account ID |
Consequências
Positivas:
- Separação clara: portal é frontend puro, plugin é API — evolução independente
- Deploy na edge (Cloudflare Workers) — latência baixa globalmente
- NextAuth abstrai complexidade de OAuth + token refresh
- Refatoração livre — sem retrocompatibilidade com versão anterior do portal
- R2 incremental cache para ISR sem custo de servidor
Negativas:
- Dois repositórios para manter (
middag-account+middag-app) - Tipos TypeScript devem ser sincronizados manualmente com DTOs da REST API
- NextAuth v4 → v5 migration planejada como parte da refatoração profunda do portal
- Dependency em
samlifypackage existente — deve ser removida (SAML removido por REF-701-02 §18) - Cloudflare Workers é target definitivo — limitações (CPU time, memory, cold starts) aceitas
Referências
- ADR-801 — Admin UI & Portal (decisão de portal separado)
- REF-801-01 §7 — Portal modules e API mapping
- ADR-701 — API & Authentication (JWT RS256, headers)
- REF-701-02 §13-16 — OAuth flows, NextAuth session, token invalidation
- ADR-901 — Integrations (Chatwoot widget no portal)