Skip to content

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

AspectoEscolha
FrameworkNext.js 16 + React 19 + TypeScript 6
AuthNextAuth v4 (JWT strategy, CredentialsProvider + Google + Azure AD)
StylingTailwind CSS v4 + Radix UI primitives
DataTanStack React Query v5 + Axios
FormsReact Hook Form + Zod validation
TablesTanStack React Table v8
i18ni18next + react-i18next + next-i18next
DeploymentOpenNext + Cloudflare Workers
BuildTurbopack (dev), Next.js default (prod)

Deployment: OpenNext + Cloudflare Workers

O portal roda em Cloudflare Workers via @opennextjs/cloudflare. Configuração:

  • output: 'standalone' no next.config.mjs
  • open-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 (binding DB)
    • R2 buckets: middag-plugins (binding STORAGE) + middag-app-opennext-cache (binding NEXT_INC_CACHE_R2_BUCKET)
    • Self-reference service binding: WORKER_SELF_REFERENCE (OpenNext caching)
    • Observability enabled

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 → dashboard

Estado 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 components

API Client Pattern

Serviços em lib/services/ encapsulam chamadas à REST API v1. Base client injeta headers automaticamente:

HeaderFonteObrigatório
AuthorizationBearer {access_token} da session NextAuthSim
X-Middag-Organizationorg do JWT payload (via session)Sim *
X-Middag-CompanyContexto 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:

ProviderTipoCallback URL
CredentialsEmail/senhaN/A (server-side)
GoogleOAuth 2.0https://app.middag.io/api/auth/callback/google
Azure ADOAuth 2.0https://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 ModuleAPI PrefixKey Operations
DashboardMultiple (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-profileProfile 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ávelDescrição
NEXTAUTH_SECRETSecret para JWT do NextAuth
NEXTAUTH_URLURL base do portal (produção)
NEXTAUTH_DEBUGtrue para debug mode (dev only)

API:

VariávelDescrição
NEXT_PUBLIC_API_URLBase URL do WP backend REST API
NEXT_PUBLIC_BASE_PATHBase path do portal (se subpath)

OAuth Providers:

VariávelDescrição
GOOGLE_CLIENT_IDGoogle OAuth Client ID
GOOGLE_CLIENT_SECRETGoogle OAuth Client Secret
AZURE_AD_CLIENT_IDAzure AD Client ID
AZURE_AD_CLIENT_SECRETAzure AD Client Secret
AZURE_AD_TENANT_IDAzure AD Tenant ID

Cloudflare (wrangler.toml, não .env):

Binding / ConfigRecurso
DB (D1)middag-app-db — edge database
STORAGE (R2)middag-plugins — object storage
NEXT_INC_CACHE_R2_BUCKETmiddag-app-opennext-cache — ISR
WORKER_SELF_REFERENCESelf-binding (OpenNext caching)
account_idCloudflare 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 samlify package 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)