Três prefeituras no mesmo banco sem vazar dados: RLS no Postgres com NestJS e Next.js
Demo técnica própria: sistema multi-tenant de gestão pública com isolamento no nível do banco, não só na query. Se um bug de aplicação esquecer o filtro, o Postgres nega assim mesmo.
Este é um case conceito — uma demo técnica construída pela Crazy Diamond, sem cliente e sem prazo comercial. Não é um sistema entregue a uma prefeitura real, e não vamos apresentá-lo como se fosse. O que ele prova é domínio de arquitetura: multi-tenancy com isolamento real, controle de papéis e deploy em produção.
Por que este case existe
A origem foi um job de ERP de gestão pública para múltiplas prefeituras, com 28 propostas concorrentes, cliente sem histórico e escopo indefinido. A decisão foi não disputar o job e construir o case próprio no lugar — porque a pergunta que trava esse tipo de contratação nunca é "você sabe fazer CRUD", é "você sabe garantir que a Prefeitura A jamais veja um dado da Prefeitura B".
A decisão técnica central
Multi-tenancy pode ser implementada com um simples filtro por tenant na query. Funciona — até o dia em que alguém escreve um endpoint novo e esquece o WHERE. O vazamento acontece em silêncio, sem erro, sem log, sem ninguém perceber.
Aqui o isolamento foi feito com Row-Level Security no Postgres, por tenant. A política vive no banco, não na aplicação. Se o código esquecer o filtro, o banco nega assim mesmo. É a diferença entre confiar na disciplina de quem escreve o código e ter uma garantia estrutural.
Não faria sentido provar "multi-tenancy" com filtro só na query. O falso-negativo silencioso é pior que o erro visível.
Como o isolamento foi validado
Foram três cenários de teste, executados tanto no banco de desenvolvimento local quanto contra o Postgres de produção via conexão pooled: um gestor municipal não consegue ler nem escrever em outro tenant; um auditor lê entre tenants mas não escreve em nenhum; e uma requisição sem contexto de sessão falha fechada — não retorna tudo, retorna nada.
Esse último ponto é o que mais costuma passar batido. Um sistema mal configurado, sem contexto, devolve o banco inteiro. Aqui ele devolve vazio.
Autenticação e papéis
JWT com access token de 15 minutos e refresh token de 7 dias rotativo, com detecção de reuso. Durante o desenvolvimento, a implementação do refresh rotativo revelou um bug real de transação que foi encontrado e corrigido — exatamente o tipo de problema que só aparece quando se implementa rotação de verdade, em vez de emitir um token novo e torcer.
Três papéis via guards: servidor (leitura), gestor (leitura e escrita apenas no próprio tenant) e auditor (leitura entre tenants, sem escrita). Acima deles, um super-admin com visão consolidada.
O que foi construído
Módulo orçamentário com rubricas, dotações e execução simplificada no fluxo empenho → liquidação → pagamento, em ledger append-only. Módulo de arrecadação tributária com cadastro de contribuintes, lançamento de tributo e status de pagamento e inadimplência.
Dashboard gerencial com KPIs, gráficos de arrecadação contra orçamento e inadimplência, além de um comparativo entre tenants visível apenas para o super-admin. E um portal de transparência público, somente leitura, com dados agregados por tenant.
O banco roda com seis migrations pareadas — cada uma com o seu rollback testado, na volta completa até a primeira. Seeds com três prefeituras fictícias, oito usuários, seis dotações e seis lançamentos tributários.
Arquitetura de front e deploy
O frontend usa arquitetura BFF: o navegador só conversa com o Next.js, que repassa o cookie ao backend no servidor. Sem CORS, sem cookie cross-site, sem token exposto no browser.
O backend NestJS foi refatorado para rodar como função serverless na Vercel, mantendo o entrypoint local e Docker funcionando sem duplicar lógica. Tudo em produção num subdomínio real, com regressão completa — login, papéis, isolamento, 404, portal público — validada contra o domínio no ar, não só contra ambiente de teste.
O que não está incluído
RH e folha de pagamento, compras e licitações, protocolo e gestão documental, e integração bancária real ficam de fora — são roadmap, não entrega. A escolha foi profundidade em dois módulos completos em vez de superfície em seis.
Stack
NestJS · Next.js 15 App Router · TypeScript strict · PostgreSQL com Row-Level Security · Neon · Tailwind · shadcn/ui · Recharts · Swagger · Vercel
