← voltar à cena

Forge Finanças

SaaS de controle financeiro para quem tem CNPJ e vida pessoal ao mesmo tempo: PF e PJ no mesmo lugar, com o imposto tratado como conta a pagar e não como categoria.

Produto próprio · beta em uso pessoal · maio a julho de 2026

O problema

Quem abre um MEI não deixa de ter vida pessoal no dia seguinte. O mercado de software financeiro, porém, insiste que sim: de um lado os aplicativos de finanças pessoais, que sabem quanto você gastou no mercado e não fazem ideia do que é um DAS; do outro os ERPs e sistemas de contabilidade, que sabem apurar imposto e tratam o aluguel da sua casa como se não existisse.

O micro-empresário brasileiro — MEI, autônomo, freelancer, a LTDA de duas pessoas — resolve isso com duas ferramentas e uma planilha de cola entre elas. E a pergunta que ele mais precisa responder não cabe em nenhuma das duas: “depois de pagar o imposto, quanto sobra pra mim?”

Este projeto ataca essa lacuna. O escopo é controle gerencial com apuração tributária embutida, para o CNPJ e para a pessoa, no mesmo lugar — sem a ambição de ERP nem a de contabilidade fiscal completa.

Uma nota sobre a origem: este SaaS foi construído com o Forge — a infraestrutura de geração de projetos que abre este portfólio. É o track APP dela rodando inteiro, do design.md ao container em produção.

Um household, dois perfis

A decisão que organiza o produto inteiro cabe numa frase: a conta é da casa, o lançamento é de um perfil. Você tem um household — que pode ser você sozinho ou você e mais alguém — e dentro dele cadastra perfis: um PF, e quantos PJ precisar, cada um com seu regime tributário (MEI, Simples Nacional, Lucro Presumido, Autônomo).

Cada lançamento pode ser marcado para um perfil, e é isso que transforma a mesma base de dados em duas visões que não se contaminam: o que entra na apuração do DAS e o que entra no Carnê-Leão são recortes da mesma tabela, não bancos separados que precisam ser reconciliados na mão.

Tela de contas do app com dois blocos separados: em cima, CONTAS, com a conta PJ do Inter Empresas e a conta PF do Nubank e o total de R$ 54.434,81; embaixo, CARTÕES DE CRÉDITO, com o cartão PJ mostrando o ciclo Fecha 20 e Vence 28 e a fatura aberta de R$ 3.180,00 em vermelho.
Duas seções, dois totais. A fatura do cartão fica fora do saldo: entra como dívida, em vermelho, com o próprio ciclo de fechamento e vencimento. Muita ferramenta empilha as duas coisas na mesma linha e produz um número que não significa nada.

O mesmo cuidado aparece em detalhes pequenos do formulário: ao escolher “Cartão de Crédito” como tipo de conta, o campo deixa de se chamar “Saldo inicial” e passa a ser “Dívida inicial — quanto você já deve neste cartão”, e dois campos de ciclo aparecem. A interface sabe o domínio em vez de servir um formulário genérico com nomes de banco de dados.

O trabalho que ninguém quer fazer

Nenhum controle financeiro morre por falta de funcionalidade. Morre porque a pessoa parou de digitar. Então o caminho mais importante do produto é o que vai do extrato do banco até o lançamento categorizado sem passar pelo teclado.

Diálogo de importação de extrato CSV mostrando a conta de destino selecionada, o aviso Prévia — 22 linhas detectadas e uma tabela de pré-visualização com datas em formato brasileiro, descrições de banco em caixa alta e valores em reais, receitas em verde e despesas em vermelho, com o botão Importar 22 lançamentos.
O import mostra o que entendeu antes de gravar qualquer coisa. As colunas foram detectadas sozinhas — `Data`, `Histórico` e `Valor`, num arquivo com separador ponto-e-vírgula — e continuam editáveis, porque detecção automática que não pode ser corrigida é uma armadilha.

O leitor foi escrito para extrato de banco brasileiro de verdade, não para o CSV ideal de um tutorial: aceita ; ou , como separador decidindo pela contagem no cabeçalho, entende dd/mm/aaaa e aaaa-mm-dd, engole o prefixo R$ e o ponto de milhar de 1.234,56, e encontra a coluna certa mesmo que o banco a tenha chamado de “Histórico”, “Memo”, “Lançamento” ou “Descripción” — a comparação é feita sem acento e sem caixa. Linha que não vira data e valor válidos é descartada em silêncio, em vez de virar um lançamento errado no meio de duzentos certos.

Depois do import entra o motor de regras.

Do extrato do banco ao lançamento já categorizado O arquivo do banco, em CSV ou OFX, passa por uma detecção automática de colunas e de separador. O resultado vira uma prévia que a pessoa confere e pode corrigir antes de qualquer gravação; linhas que não produzem data e valor válidos são descartadas nessa etapa e não viram lançamento. Ao confirmar, cada linha atravessa o motor de regras, que testa os padrões cadastrados em ordem de prioridade — o menor número roda primeiro — usando comparação por texto contido ou por expressão regular. A regra que casa preenche a categoria e o perfil PF ou PJ, e só então o lançamento é gravado. do extrato ao lançamento categorizado arquivo do banco CSV ou OFX detecta colunas e o separador linha ilegível descartada aqui prévia você confere antes confirmar motor de regras por prioridade texto contido ou regex lançamento categoria e perfil Nada é gravado antes da confirmação: a prévia é o ponto de correção, não um aviso decorativo.
O motor é determinístico de propósito. Cada regra é um padrão — texto contido ou expressão regular — com uma prioridade, e a de menor número roda primeiro; a primeira que casa preenche categoria e perfil. Nada de "o sistema aprendeu": a pessoa consegue explicar por que aquele lançamento caiu naquela categoria, e mudar isso editando uma linha.

Na conta que montei para estes prints, nove regras classificaram os vinte e dois lançamentos importados de uma vez — inclusive duas por expressão regular, uma para separar assinatura de software (INTERNET FIBRA|ASSINATURA|FIGMA) e outra para reconhecer entrada de nota fiscal (PIX RECEBIDO.*NF\s*\d+) e mandá-la para Serviços Prestados, que é a categoria que alimenta a apuração.

Lista de lançamentos com colunas Data, Descrição, Tipo, Categoria, Conta e Valor. Descrições cruas de extrato bancário aparecem com categoria já preenchida: PIX RECEBIDO CLINICA VITA como Serviços Prestados, FARMACIA PAGUE MENOS como Saúde, IFOOD como Alimentação, INTERNET FIBRA VIVO EMPRESAS e ASSINATURA FIGMA como Software e SaaS, ALUGUEL SALA COMERCIAL como Aluguel.
O resultado do mesmo arquivo depois das regras. `IFOOD *IFOOD` e `FARMACIA PAGUE MENOS` são o texto exato que o banco manda; a coluna da direita é o que a pessoa realmente quer ler.

A árvore de categorias que vem semeada já carrega a tese do produto: ao lado de Moradia, Alimentação e Saúde existem Custos Operacionais — com Contabilidade, Marketing e Software — e Impostos e Taxas, com Carnê-Leão e DAS / MEI como subcategorias de verdade. A separação PF e PJ não é um discurso da página de vendas; está no dado padrão que o usuário recebe no primeiro minuto.

Imposto não é categoria — é conta a pagar com data

Aqui está o que nenhum aplicativo de finanças pessoais faz e o que nenhum ERP barato faz bem.

O sistema calcula DAS-MEI (nas três naturezas: comércio, serviços e comércio com serviços), Carnê-Leão pela tabela progressiva e Simples Nacional pelos anexos III e V. E calcula com valores oficiais, não com aproximação: o salário mínimo de 2026 está fixado em R$ 1.621,00 com o decreto citado no próprio código (12.797/2025), e o teste do DAS-MEI de serviços afirma o resultado exato — R$ 86,05, decomposto em INSS de 5% sobre o mínimo (R$ 81,05) mais ISS de R$ 5,00. Trocar a tabela de um ano quebra o teste, que é exatamente o que se quer de uma regra que muda por decreto.

Calculado o valor, ele vira obrigação com vencimento, e daí sai o que mais parece útil no uso diário: um calendário tributário do ano inteiro.

Tela Calendário Tributário com seletor de ano em 2026 e uma legenda de duas cores: obrigação fiscal em âmbar e feriado nacional em vermelho. Abaixo, os doze meses em grade, com o dia 20 marcado em âmbar em todos os meses e feriados em vermelho, incluindo 16 e 17 de fevereiro no carnaval.
O ano fiscal inteiro numa tela. O dia 20 marcado em todo mês é o vencimento do DAS; o vermelho é feriado nacional. O calendário de feriados é a mesma base que a projeção consulta para saber que uma receita que cai num sábado só entra na segunda.

Três coisas ficam claras nessa tela e nenhuma delas é óbvia de construir. A primeira é que existe um calendário de dias úteis brasileiro no sistema, com feriados nacionais, e ele é consultável por API própria. A segunda é que uma obrigação pode ser marcada como paga e tem uma guia DARF gerável em PDF. A terceira é que existe um job diário que avisa antes do vencimento — porque a multa por atraso do DAS não perdoa quem esqueceu.

O isolamento que não depende do WHERE

Multi-tenancy em SaaS financeiro tem um modo de falhar que acaba com a empresa: uma query esquece o filtro do inquilino e alguém vê o extrato de outra pessoa. A defesa usual — “todo mundo lembra de filtrar” — é uma promessa, não um mecanismo.

Aqui são duas camadas independentes, e nenhuma delas depende de o desenvolvedor lembrar.

As duas camadas de isolamento entre households O identificador do household chega no token do pedido autenticado e alimenta duas defesas independentes. A primeira é um filtro global do Entity Framework, aplicado por reflexão a toda entidade que implementa a interface de escopo por household, o que acrescenta a condição a qualquer consulta sem que o desenvolvedor precise escrevê-la. A segunda é a política de Row Level Security do próprio Postgres, ativada com FORCE em dezenove tabelas e escrita com USING e WITH CHECK, de modo que vale tanto para leitura quanto para escrita; o valor é injetado a cada transação por uma chamada parametrizada a set_config com escopo local. As duas convergem no mesmo resultado, e uma falha em qualquer uma delas não abre a outra. duas camadas para a mesma pergunta pedido autenticado household vem no token filtro global do EF aplicado por reflexão política RLS do Postgres USING e WITH CHECK vira condição na query sem ninguém lembrar vale no banco, não no app 19 tabelas com FORCE só as linhas do household leitura e escrita Independentes de propósito: um bug numa não abre a outra.
A primeira camada é aplicação, a segunda é banco. O FORCE importa: sem ele a política não vale para o dono da tabela, que é justamente a conexão da aplicação. E o valor entra na sessão por chamada parametrizada com escopo de transação — nunca por concatenação de string em SQL.

Das 28 tabelas do schema, 19 estão sob FORCE ROW LEVEL SECURITY, com a política escrita em USING e WITH CHECK — ou seja, ela impede tanto ler linha alheia quanto gravar linha com o household de outro. Treze arquivos de teste de integração exercitam vazamento entre households, e o arquivo dedicado a isso é um gate obrigatório da suíte.

O mesmo rigor vale para o dado mais sensível do cadastro. CPF e CNPJ são guardados com AES-256-GCM e chave derivada por household (HKDF, com o identificador do household como dado autenticado adicional), e ao lado do campo cifrado mora um HMAC-SHA256 do mesmo valor, que existe só para permitir busca sem descriptografar nada. A tela diz isso ao usuário no momento em que ele digita: “Armazenado criptografado (AES-GCM). Nunca exibido em claro após salvar.”

O que já é produto, e não protótipo

A diferença entre um app de portfólio e algo que dá para cobrar por está quase toda em coisas que ninguém demonstra numa tela bonita.

Dashboard do app com o menu lateral completo de quinze itens visível — Dashboard, Contas, Lançamentos, Fontes de Renda, Auto-categorização, Categorias, Perfis PF/PJ, Orçamentos, Projeção, Investimentos, Calendário Fiscal, Dias Úteis, Relatórios, Família e Plano — e três widgets montados pelo usuário: saldo total de R$ 57.614,81 em três contas, um gráfico de barras de receita contra despesa com líquido de R$ 14.687,65, e uma lista de top categorias encabeçada por Aluguel.
O dashboard é montado pelo usuário a partir de seis widgets e o layout fica salvo na conta dele. À esquerda, a extensão real do produto — a barra lateral é o argumento de escopo que nenhum parágrafo substitui.

Estão prontos e testados: autenticação com segundo fator TOTP; planos com limites aplicados no servidor — ao tentar criar a quarta conta no plano gratuito, a API responde com recusa e a interface exibe um aviso com link de upgrade, em vez de estourar um erro cru; tokens de API pessoais e webhooks assinados com HMAC com política de retentativa, que é o que permite a alguém integrar o próprio sistema; exportação e exclusão de conta para LGPD, com anonimização; relatórios em PDF e XLSX gerados de forma assíncrona; e trilha de auditoria.

Três decisões de produto merecem destaque separado.

O token de acesso nunca toca o localStorage. Só o estado não-sensível é persistido no navegador; o token vive em memória. É uma escolha que custa conveniência — recarregar a página pede login de novo — e compra imunidade à classe inteira de ataque em que um XSS rouba a sessão. Descobri isso na prática enquanto navegava para tirar os prints, o que é a melhor forma de confirmar que a regra é real.

Nada além do proxy é exposto. Em produção o docker ps mostra uma única porta publicada, a do Caddy. O backend .NET e o Postgres não têm porta mapeada para o host — só a rede interna do compose os alcança. O mesmo Caddy serve a SPA e faz proxy da API no mesmo domínio, o que faz o cookie de refresh SameSite=Strict funcionar e elimina CORS do caminho, e ainda aplica HSTS, nosniff, X-Frame-Options e Referrer-Policy em toda resposta.

Existe um assistente por WhatsApp. Uma mensagem em português — “gastei 45 no mercado” — passa por um interpretador de intenção rodando em Claude Haiku e vira lançamento, consulta de saldo ou consulta de gastos, sempre escopada ao household do número vinculado, com verificação de vínculo. É o caminho de entrada mais curto que existe para o problema que mata este tipo de produto: a pessoa parar de registrar.

As decisões e o que cada uma custou

Nenhuma das escolhas abaixo é gratuita, e o projeto registra o preço de cada uma.

Banco compartilhado com RLS, não um schema por inquilino. Compra operação simples, uma migration só e custo por cliente baixo o bastante para um SaaS de ticket pequeno fechar a conta. O preço é que uma falha de política vaza dados — e é por isso que existe a segunda camada e o teste de vazamento como gate.

Dinheiro em NUMERIC(18,4) com a moeda ao lado, em vez de inteiro em centavos. Ler e exibir sem conversão, e multimoeda desde o primeiro dia. O lado ruim são uns 10% a mais de espaço que um BIGINT, que num sistema deste porte é troco.

Categorias em ltree. Árvore sem limite de profundidade, e a agregação de uma subárvore inteira numa query só em vez de uma consulta por nível. Em troca, mudar um nó de pai vira atualização em massa, e o projeto fica preso ao Postgres — uma dependência que já estava na conta quando a decisão foi tomada.

CQRS com handlers explícitos por injeção, sem MediatR. Esta é a que costuma ser tomada por hábito: o MediatR passou a exigir licença comercial a partir da versão 12. Trocar a biblioteca por chamadas diretas custou escrever os comportamentos transversais à mão, como filtros de endpoint — e removeu do produto uma dependência que poderia virar uma fatura. De quebra, o caminho do código ficou literal: dá para ler do endpoint até o banco sem procurar quem assina o quê.

Cobrança com a maquinaria pronta e o meio de pagamento adiado. Os limites de plano já são aplicados e testados, e as tabelas de plano e assinatura já existem — plugar um gateway depois não migra dado. O preço é honesto e está dito: não há receita real até esse plug acontecer.

Estado

No ar num deploy de produção local, atrás de proxy e com migrations aplicadas no boot. A API expõe 96 endpoints em 71 caminhos, sobre 28 tabelas e 26 migrations do EF Core. São cerca de 45 mil linhas de C# no backend e 15 mil de TypeScript no frontend, com 518 métodos de teste em xUnit (unidade, integração e testes de arquitetura) e 196 casos em Vitest, além de suítes Playwright para os fluxos de autenticação e de lançamentos.

A visita guiada acima foi feita numa conta de teste populada do zero pela própria interface: dois perfis, três contas, um extrato de 22 linhas importado por CSV e nove regras de categorização — que é, com alguma sorte, o mesmo caminho que um usuário novo percorre na primeira hora.

Quer conversar sobre este projeto — ou sobre construir algo assim? Fale comigoou escreva para contato@daniel-dac.com.

Ver os outros projetos