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.product | Descrição |
|---|---|---|
emplacamento | emplacamento (ou vazio/legado) | Vona / Associação Digital — produto original, também o default |
belite | belite | Relatórios F&I |
intranet | intranet | Gestão de documentos |
operacional | admin | Back-office administrativo (nome de pasta ≠ nome do produto no banco) |
Atenção —
operacional(pasta) ≠admin(produto no banco) O back-office fica na pastaoperacional/, mas o valor gravado empublic_data.productpara essas contas éadmin.PRODUCT_PATHSempaths.config.tsmapeia a chaveadminparaoperacionalPaths. Não confundir com/adminna raiz, que é o Super Admin do kit (painel interno do MakerKit, gate poris_super_admin), completamente à parte do produtooperacional.
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/emembers/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):
-
products.config.ts— adicionar o id emProductId(se ainda não estiver) e uma entrada emPRODUCTScomlabelefeatures. -
paths.config.ts— criarxPaths = createProductPaths('x')e registrar emPRODUCT_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 empublic_data.product(como aconteceu comoperacional/admin), a chave emPRODUCT_PATHSdeve ser o valor do banco — não o nome da pasta. -
Criar a árvore de rotas em
app/[locale]/x/home/[account]/:layout.tsx→createTeamWorkspaceLayout(XSidebar)(verbelite/home/[account]/layout.tsxcomo modelo)_components/app-sidebar.tsx(ou nome equivalente) → sidebar específico do produto, implementandoTeamWorkspaceSidebarPropspage.tsx→ landing do produto (redireciona pra feature principal, comobelite/home/[account]/page.tsxfaz)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
-
proxy.ts— adicionar'x'emPRODUCT_ROUTE_PREFIXES, para quehomeAuthGuard(o gate de autenticação de/<produto>/home/*) cubra a rota nova automaticamente. -
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/(recebendoTeamWorkspaceSidebarProps). Criar esse componente com os itens de menu do produto novo. -
Dispatcher cross-produto — atualizar
emplacamento/home/[account]/page.tsxpara 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(vialoadAccountsMeta()) 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.