Pular para o conteúdo principal

Arquitetura de rotas por produto no monorepo — Next.js App Router

O superapp hospeda vários produtos (emplacamento, belite, intranet, operacional) num único app Next.js, cada um com sua própria pasta de topo em app/[locale]/. Um shell compartilhado (_shared/team-workspace) concentra as features comuns a todos (settings, membros, billing) para evitar duplicação, enquanto cada produto mantém suas próprias features de negócio. Este documento mapeia a árvore atual e descreve o passo a passo para adicionar um produto novo (TEC-448).

Por que essa estrutura existe

Cada team account (public.accounts) é marcada com um produto em public_data.product (ver apps/web/config/products.config.ts). Trocar de conta no seletor pode significar trocar de produto inteiro — layout, sidebar, features de negócio. Antes do TEC-448 tudo vivia sob uma única árvore de rotas; agora cada produto tem sua pasta de topo própria em app/[locale]/, o que:

  • Isola as features de negócio de cada produto (relatórios do Belite não colidem com documentos da Intranet)
  • Mantém o roteamento explícito: a URL já diz o produto (/belite/home/[account]/...)
  • Permite que features comuns (settings, membros, billing) fiquem em um único lugar (_shared/team-workspace), reexportadas por cada produto

Os 4 produtos atuais:

Produto (pasta de rota)public_data.productDescrição
emplacamentoemplacamento (ou vazio/legado)Vona / Associação Digital — produto original, também o default
belitebeliteRelatórios F&I
intranetintranetGestão de documentos
operacionaladminBack-office administrativo (nome de pasta ≠ nome do produto no banco)

Atenção — operacional (pasta) ≠ admin (produto no banco) O back-office fica na pasta operacional/, mas o valor gravado em public_data.product para essas contas é admin. PRODUCT_PATHS em paths.config.ts mapeia a chave admin para operacionalPaths. Não confundir com /admin na raiz, que é o Super Admin do kit (painel interno do MakerKit, gate por is_super_admin), completamente à parte do produto operacional.

ProductId (products.config.ts) hoje inclui também ganhaz, que existe no registro de produtos mas ainda não tem pasta de rota própria — contas desse produto caem no fallback (emplacamento) até a pasta ser criada.

Árvore de pastas atual

app/[locale]/
├── emplacamento/ # Produto Vona/Associação Digital (default)
│ ├── (marketing)/ # Marketing público (rota group)
│ ├── (extranet_public)/ # Sites públicos por cliente (abracaf, ganhaz, fidelidade, vona, ...)
│ ├── (dev)/ # Previews dev-only (bloqueadas fora de development)
│ ├── docs/ # Docs internas do produto
│ └── home/
│ ├── (user)/ # Conta pessoal
│ ├── create-team/
│ └── [account]/ # Conta de time (slug) — DISPATCHER de produto
│ ├── page.tsx # Lê public_data.product e redireciona
│ ├── _components/
│ ├── _lib/server/
│ ├── reports/ # Feature própria do emplacamento (Vona)
│ ├── billing/{page,layout,error,return/page}.tsx → reexport _shared
│ ├── settings/{page,layout,profile/page}.tsx → reexport _shared
│ └── members/{page,policies/route}.ts → reexport _shared

├── belite/home/[account]/
│ ├── page.tsx # Redireciona pra relatorios/definir-nome-1
│ ├── layout.tsx # createTeamWorkspaceLayout(AppSidebar)
│ ├── _components/app-sidebar.tsx # Sidebar específico do Belite
│ ├── relatorios/
│ │ ├── definir-nome-1/ # Feature própria (relatório F&I)
│ │ └── definir-nome-2/
│ ├── billing/{page,layout,error,return/page}.tsx → reexport _shared
│ ├── settings/{page,layout,profile/page}.tsx → reexport _shared
│ └── members/{page,policies/route}.ts → reexport _shared

├── intranet/
│ ├── (dev)/documentos-preview/ # Preview dev-only
│ ├── _lib/{schema,server}/
│ └── home/[account]/
│ ├── documentos/ # Feature própria (gestão de documentos)
│ ├── billing/{page,layout,error,return/page}.tsx → reexport _shared
│ ├── settings/{page,layout,profile/page}.tsx → reexport _shared
│ └── members/{page,policies/route}.ts → reexport _shared

├── operacional/home/[account]/ # Back-office (public_data.product === 'admin')
│ ├── clientes/ # Feature própria
│ ├── produtos/ # Feature própria
│ ├── _components/admin/
│ ├── _lib/server/
│ ├── billing/{page,layout,error,return/page}.tsx → reexport _shared
│ ├── settings/{page,layout,profile/page}.tsx → reexport _shared
│ └── members/{page,policies/route}.ts → reexport _shared

├── admin/ # Super Admin do KIT (raiz, is_super_admin) — não é o produto operacional
│ ├── accounts/[id]/
│ └── _components/

├── [team]/ # Login multi-cliente por domínio custom (abracaf, belite.com.br, ...)
│ ├── layout.tsx
│ ├── _components/, _lib/
│ └── auth/
│ ├── sign-in/, sign-up/, verify/, password-reset/
│ └── callback/{route.ts,error/page.tsx}

├── _shared/team-workspace/ # Shell compartilhado pelos 4 produtos
│ ├── team-workspace-layout.tsx # createTeamWorkspaceLayout(Sidebar) factory
│ ├── _components/ # navigation menu, mobile nav, page header, seletor de contas
│ ├── _lib/server/ # loadTeamWorkspace, load-account-product (loadAccountsMeta)
│ ├── billing/ # team-account-billing-{page,layout,error}, return/
│ ├── settings/ # team-account-settings-{page,layout}, profile/
│ └── members/ # team-account-members-page, policies/

└── api/ # API routes

Padrão de reexport billing/, settings/ e members/ de cada produto não implementam nada — são arquivos finos que reexportam o componente equivalente de _shared/team-workspace. Exemplo real (belite/home/[account]/settings/page.tsx):

export {
default,
generateMetadata,
} from '~/_shared/team-workspace/settings/team-account-settings-page';

Isso é o que garante que uma mudança em settings/billing/membros afete os 4 produtos ao mesmo tempo, sem duplicar código.

Diagrama

Como adicionar um novo produto

Passo a passo para criar um produto novo (ex: ganhaz, que já existe no registro mas ainda não tem pasta de rota):

  1. products.config.ts — adicionar o id em ProductId (se ainda não estiver) e uma entrada em PRODUCTS com label e features.

  2. paths.config.ts — criar xPaths = createProductPaths('x') e registrar em PRODUCT_PATHS:

    export const ganhazPaths = createProductPaths('ganhaz');

    const PRODUCT_PATHS: Record<string, ReturnType<typeof createProductPaths>> = {
    emplacamento: pathsConfig,
    belite: belitePaths,
    intranet: intranetPaths,
    ganhaz: ganhazPaths,
    admin: operacionalPaths,
    };

    Atenção — chave de PRODUCT_PATHS é o valor do banco, não a pasta Se o produto novo precisar de um nome de pasta diferente do valor gravado em public_data.product (como aconteceu com operacional/admin), a chave em PRODUCT_PATHS deve ser o valor do banco — não o nome da pasta.

  3. Criar a árvore de rotas em app/[locale]/x/home/[account]/:

    • layout.tsxcreateTeamWorkspaceLayout(XSidebar) (ver belite/home/[account]/layout.tsx como modelo)
    • _components/app-sidebar.tsx (ou nome equivalente) → sidebar específico do produto, implementando TeamWorkspaceSidebarProps
    • page.tsx → landing do produto (redireciona pra feature principal, como belite/home/[account]/page.tsx faz)
    • billing/{page,layout,error,return/page}.tsx, settings/{page,layout,profile/page}.tsx, members/{page,policies/route}.ts → arquivos finos de reexport para ~/_shared/team-workspace/... (copiar o padrão de qualquer produto existente)
    • Pasta(s) com as features de negócio próprias do produto
  4. proxy.ts — adicionar 'x' em PRODUCT_ROUTE_PREFIXES, para que homeAuthGuard (o gate de autenticação de /<produto>/home/*) cubra a rota nova automaticamente.

  5. Navegação/sidebar — não existe um registry central de navegação por produto; cada produto define seu próprio componente de sidebar em _components/ (recebendo TeamWorkspaceSidebarProps). Criar esse componente com os itens de menu do produto novo.

  6. Dispatcher cross-produto — atualizar emplacamento/home/[account]/page.tsx para redirecionar contas do produto novo:

    if (meta[account]?.product === 'x') {
    redirect(`/x/home/${account}/<feature-principal>`);
    }

    Isso é necessário porque o seletor de conta sempre navega para /emplacamento/home/[account] primeiro — esse dispatcher lê public_data.product (via loadAccountsMeta()) e decide o home real.

Gotcha: error.tsx precisa do próprio 'use client'

Next.js exige que o arquivo de convenção error.tsx tenha a diretiva "use client" no próprio arquivo — ela não se propaga através de uma cadeia de reexport, mesmo que o componente reexportado já seja client component.

Por isso _shared/team-workspace/billing/team-account-billing-error.tsx tem 'use client', e cada error.tsx fino de produto precisa repetir a diretiva como primeira linha, mesmo sendo só um reexport:

'use client';

export { default } from '~/_shared/team-workspace/billing/team-account-billing-error';

Omitir o 'use client' no arquivo do produto quebra silenciosamente — o Next.js não avisa, o error boundary simplesmente não funciona como client component.