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.
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.
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.