Arquitetura e Design Patterns — Superapp
Documento de referência sobre a arquitetura do projeto e os padrões de código que se repetem nele. Escrito para um dev novo no time se situar rápido.
1. Visão geral
O projeto é um superapp multi-produto construído sobre o MakerKit —
Next.js + Supabase SaaS Starter Kit (Turbo Edition) (next-supabase-saas-kit-turbo).
Um único deploy hospeda vários produtos (emplacamento, belite, intranet,
operacional, admin), cada um com seu próprio conjunto de features, sobre uma
base comum multi-tenant (contas, membros, papéis, billing).
- Produto = configuração / feature-set (
config/products.config.ts). - Tenant = uma team account (
public.accounts) marcada com seu produto empublic_data.product. - Uma organização em vários produtos = várias contas, uma por produto.
2. Arquitetura
O projeto adota uma arquitetura modular em camadas organizada como um monorepo package-by-feature, com um modelo multi-tenant na base e uma camada multi-produto por cima.
São três ideias que se combinam:
Monorepo modular (package-by-feature). Cada capacidade transversal — auth,
billing, notificações, acesso a dados, UI, i18n, e-mail, monitoramento — é um
pacote independente sob packages/@kit/*, com fronteiras explícitas. A
aplicação web (apps/web) consome esses pacotes. Isso mantém baixo o
acoplamento entre capacidades e permite evoluir cada uma isoladamente.
Camadas (layered). Dentro de cada rota, a responsabilidade é dividida em três camadas bem separadas:
| Camada | Onde vive | Papel |
|---|---|---|
| Apresentação | page.tsx, _components/ | Interface com o usuário |
| Serviço/Domínio | _lib/server/*.service.ts | Regra de negócio |
| Dados | Supabase client + RLS | Persistência e autorização |
A entrada de dados vinda do usuário passa por Server Actions e Route Handlers, que orquestram a chamada às camadas de serviço e dados.
Multi-tenant + multi-produto. A base é multi-tenant: contas pessoais e de time, membros, papéis e permissões, com autorização aplicada no banco via Row Level Security (RLS). Sobre essa base, uma camada multi-produto permite que um único deploy sirva vários produtos, cada um com seu feature-set, resolvidos em tempo de execução a partir da configuração da conta.
3. Estilo de acesso a serviços externos
Serviços externos com mais de um fornecedor são acessados por trás de uma interface comum, permitindo trocar a implementação por configuração sem afetar quem consome:
- Billing (
@kit/billing-gateway): Stripe e Lemon Squeezy expostos pela mesma interface. - E-mail (
@kit/mailers): um provedor para desenvolvimento e outro para produção, selecionados por variável de ambiente.
O acesso ao banco de dados, por outro lado, é feito diretamente pelo client do Supabase dentro da camada de serviço — uma escolha deliberada, já que o banco é uma dependência estável do projeto e não há intenção de trocá-lo. A consequência prática é que a camada de serviço conhece o client do Supabase; em troca, evita-se a complexidade de abstrair o acesso a dados atrás de interfaces genéricas.
4. Estrutura do monorepo
apps/ packages/ (@kit/*)
├── web (Next.js 16) ├── features/ (auth, accounts, team-accounts, admin, notifications)
├── e2e (Playwright) ├── billing/ (core, gateway, stripe, lemon-squeezy)
└── dev-tool ├── supabase, next, ui, i18n, mailers, monitoring, cms...
app/[locale]/
├── emplacamento/ → dispatcher central + produto Vona
├── belite/ intranet/ operacional/ → produtos
├── admin/ → super admin do kit
├── [team]/ → login multi-cliente por domínio custom
└── _shared/team-workspace/ → shell + createTeamWorkspaceLayout(Sidebar)
5. Padrões estruturais macro
- Registry —
PRODUCTSemproducts.config.tseCLIENTSemclients.config.ts: dicionários centrais resolvidos em runtime por chave. - Factory de layout —
createTeamWorkspaceLayout(Sidebar)produz o layout de cada produto, parametrizado pelo Sidebar específico. - Dispatcher (Front Controller) —
/emplacamento/home/[account]lêpublic_data.producte redireciona para o home do produto certo.
6. O design pattern dominante do código
O trio que se repete em quase todo produto:
Service Layer + Factory + Injeção de Dependência (DI).
Em português:
- Service Layer: a regra de negócio de cada assunto fica numa classe
XService, separada da tela e do banco. - Factory: uma função
createXService(client)constrói o serviço pronto — ninguém dánewdireto. - Injeção de Dependência: o serviço não cria a conexão com o banco; ela é entregue de fora (pela server action) no momento da criação. Isso torna o serviço testável (injeta-se um client falso no teste).
Para um passo a passo didático — o trio explicado peça a peça, como um relatório de BI é montado de ponta a ponta, e as convenções de frontend (Server Component orquestrador,
_components//_lib/, lazy loading, componentes compartilhados e oCommonGridTable) — veja Convenções de código. Recomendado para quem está chegando agora no projeto.
Exemplo canônico
// factory + classe (goals.service.ts)
export function createGoalsService(client: SupabaseClient<Database>) {
return new GoalsService(client);
}
class GoalsService {
constructor(private readonly client: SupabaseClient<Database>) {}
async listGoals(accountId: string) {
return this.client.from('goals')/* ... */;
}
}
// uso na server action — injeta o client
const service = createGoalsService(getSupabaseServerClient());
Fluxo completo
Tela (React)
│ usuário clica
▼
Server Action ── cria o client (getSupabaseServerClient) e injeta na factory
▼
Factory (createXService) ── monta e devolve o Service pronto
▼
Service (XService) ── regra de negócio, usa o client injetado
▼
Supabase / Postgres (RLS cuida da permissão)
7. Conformidade ao padrão (estado atual)
| Situação | Onde | Observação |
|---|---|---|
| Segue o trio completo (~13 services) | intranet (polls, blog, publications, documents, events, usuarios), belite (goals, usuarios), billing (team/user), identities (primeiro-acesso) | Padrão dominante e consistente |
| Parcial — injeta o client normal mas cria o admin client dentro dos métodos | belite/usuarios, intranet/usuarios, team-billing, audit | Custa testabilidade |
| Fora do padrão — funções soltas, sem service/factory | belite monitor-fi (fi-objetivo.ts, fi-evolucao-fabricante.ts) | Também é onde o typecheck quebra |
| Estilo alternativo — action-cêntrico, sem service layer | operacional (back-office) | Decisão consciente para CRUD administrativo |
8. Trade-offs e implicações de negócio
- Acoplamento baixo entre produtos na aplicação: o código de
belite/não importa nada deemplacamento/(verificado). Cada produto é isolado nas suas pastas. - Acoplamento alto na plataforma: banco único (33 schemas compartilhados), auth, roteamento e config são centralizados.
- Separar um produto em ambiente próprio é de esforço médio a alto, dominado pelo fatiamento do banco compartilhado e pela centralização do auth/roteamento — não pelo código de aplicação, que está saudável.
9. Referências no código
config/products.config.ts— registry de produtos,ProductId.config/clients.config.ts— registry de clientes (tenants).app/[locale]/_shared/team-workspace/team-workspace-layout.tsx— factory de layout.apps/web/supabase/schemas/— 33 schemas (core MakerKit + tabelas de produto).