Logo Crazy Diamond — sistema de gestão pública multi-tenant
Case· Crazy Diamond (demo técnica)

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