root@construct:~/projects/topo$
<-- voltar para /projects

TOPO Contábil

Plataforma de operação contábil para grupos com várias empresas

TypeScriptNestJSNext.jsPrismaPostgreSQLRedis

>Contexto

A Topo é uma plataforma modular que roda a operação contábil de grupos com várias empresas em cima do ERP deles: conciliação de contas com revisão, aprovação e prazos em dias úteis, gestão do fechamento, obrigações acessórias e certidões, demonstrações financeiras, auditoria externa e consolidação. A ideia é dar padronização, controle, visibilidade e rastreabilidade a essa operação.

Cofundei a Topo em outubro de 2025 com um contador. Construí a arquitetura de produto, a engenharia e a segurança. Hoje são 8 módulos de produto construídos, incluindo um assistente sobre Claude.

Os dados são de finanças corporativas, então o isolamento entre clientes fica no banco, e a trilha de auditoria só aceita inclusão. A Topo chegou a ARR de cinco dígitos no primeiro ano, com clientes B2B pagantes e sem gasto com distribuição.

>Arquitetura (HLD)

As seis telas (app, painel admin, site, docs, portal do desenvolvedor e wiki) são apps Next.js 16 na Vercel. A API é um serviço NestJS 12 num container Docker na Railway, com três réplicas no ar, e o PostgreSQL, o Redis e o MinIO rodam também na Railway. A Cloudflare responde pelos nomes do domínio e fica na frente da API.

O navegador carrega a tela da Vercel e depois pede os dados direto à API, com o login num cookie que o código da tela não lê. ERPs e ferramentas de BI entram pela API pública v1 com chave de API, e agentes de IA pelo conector MCP com OAuth, servido pelo mesmo processo da API. A API confere login, cliente e papel, e cada consulta ao PostgreSQL leva o cliente marcado na transação, para a RLS esconder as linhas dos outros. Nenhuma tela fala com o banco, e os anexos ficam no MinIO sob o cliente dono. Importação de balancete, operações em lote, sincronização de períodos e e-mails rodam em filas Bull no Redis, consumidas pelo próprio processo da API; as tarefas agendadas usam uma trava no Redis para não rodar duas vezes. O assistente chama o Claude pelo SDK da Anthropic, e os erros da API e das telas vão para o Sentry.

Push e merge não publicam nada. Um script de publicação grava a versão num arquivo do repositório e faz o push, e a Railway só constrói a imagem quando esse arquivo muda. Antes de a API nova ligar, um passo único sincroniza o schema e reaplica a RLS; se sobrar tabela de cliente sem a regra, a publicação para e a API nova não liga. Em produção a API sobe primeiro, e o script só chama o deploy hook das telas na Vercel depois que a API responde o commit novo. Staging é um ambiente separado, com uma réplica e dados próprios, e produção só recebe o que já passou por staging, fora correção de emergência.

Passe o mouse ou foque uma peça: ela acende com as ligações.
quem usaVercelRailway (produção)código e entregaserviços externos1 Equipe contábil2 Agentes de IA3 DNS e proxy10 GitHub5 API NestJS 124 Telas Next.js 168 PostgreSQL11 ERP e BI do cliente6 Filas e travas13 Modelo do assistente12 Rastreio de erros14 E-mail transacional7 Workers e crons9 Evidências
14 peças14 ligações6 áreas

Componentes

  • Telas na Vercel :: Seis apps Next.js 16 que só são construídos quando o script de publicação chama o deploy hook de cada um; o navegador fala com a API e manda erros ao Sentry e uso de tela ao PostHog.
  • API NestJS na Railway :: Container Docker com três réplicas e health check. Em cada requisição confere login, cliente, papel e módulo, e serve a API pública v1 (REST e GraphQL só leitura) e o conector MCP em HTTP.
  • PostgreSQL com RLS forçada :: Acessado só pela API, via Prisma 7, com o cliente marcado em cada transação e a política do banco filtrando as 120 tabelas de cliente.
  • Filas e workers no Redis :: Filas Bull 4 para lotes, importação de balancete, sincronização de períodos e e-mail, consumidas pelo processo da API, e travas que impedem um cron de rodar duas vezes.
  • Evidências no MinIO :: Armazenamento compatível com S3 na Railway, com cada arquivo guardado sob o cliente dono e entregue só pela API depois da checagem de permissão.
  • Assistente sobre Claude :: A API chama o Claude pelo SDK da Anthropic, com ferramentas que leem pelos repositórios sujeitos à RLS e um teto de custo mensal por cliente.
  • Esteira de publicação :: GitHub Actions roda os checks de cada PR e uma cópia cifrada mensal do banco e dos anexos; um script versiona a release, a Railway constrói a API pelo Dockerfile e as telas sobem pelos deploy hooks da Vercel.

>Trade-offs

Isolar os tenants no banco, com row-level security ligada e forçada em toda tabela que tem tenant.

Alternativa descartada: Confiar só no filtro por tenant escrito em cada consulta da aplicação.

Por quê: Um WHERE esquecido numa consulta mostraria a contabilidade de uma empresa para outra. A política do banco vale para toda consulta, inclusive as que ninguém lembrou de revisar. Sem o FORCE, o dono da tabela passaria por cima das próprias políticas.

Um teste que falha se qualquer modelo com tenant não tiver RLS ligada, forçada e com política, sem lista de exceções.

Alternativa descartada: Uma lista de tabelas liberadas da regra, cada uma justificada na hora.

Por quê: Uma lista de exceções cresce com itens que parecem defensáveis um a um e nunca são revistos. Tabela que precisa de outra regra recebe uma política própria em vez de ficar sem nenhuma.

Result<T, E> e ValidationResult com severidade no domínio e na aplicação.

Alternativa descartada: Exceções com try/catch para as falhas de negócio.

Por quê: Separa falha esperada de erro do sistema, deixa o tratamento explícito em cada chamada e permite que a tela explique o motivo de uma recusa, como a mesma pessoa preparar e aprovar uma conciliação. O custo é código mais verboso e o controller precisa desembrulhar o resultado.

Clean Architecture com domínio sem dependência externa e fronteiras checadas no CI.

Alternativa descartada: Regras de negócio escritas direto nos serviços do framework e nas chamadas do ORM.

Por quê: As regras contábeis são testadas sem banco e sem framework. O preço é cerimônia: uma feature simples pede vários arquivos e uma camada de mappers entre domínio, banco e DTO.

Módulo novo nasce desligado: entra no catálogo e no contrato, mas sem rota nem tela até ser ligado, e o acesso é negado por padrão.

Alternativa descartada: Publicar o módulo visível assim que o código chega em produção.

Por quê: A parte comercial (pacotes, preço, concessão por empresa) anda sem depender de a engenharia ter terminado, e nenhum cliente vê um módulo pela metade.

>Destaques

  • >ARR de cinco dígitos no primeiro ano, sem gasto com distribuição
  • >Row-level security forçada nas 120 tabelas de tenant, com teste sem exceção
  • >94,7% de respostas corretas do assistente em 38 casos reservados, nas duas repetições
  • >Mais de 3.500 arquivos de teste e um gate de 9 passos antes de cada publicação
  • >372 versões publicadas e 2.054 PRs mergeados desde outubro de 2025
  • >Importação de 3.000 empresas caiu de 262 s para 36 s

>Curiosidades

  • *Quando troquei de máquina, a compilação completa da Topo a frio passou a levar 23 segundos num M4 Max.
  • *Os ADRs da Topo pulam do 004 para o 006. O 005 nunca foi usado, e o buraco ficou para não renumerar os outros.
  • *O main da Topo tem 8.382 commits, o primeiro de 9 de outubro de 2025.
TOPO Contábil | The Broad Way | Kinho.dev