Todos os projetos
Produto próprioSaaS em produção

CRM que se molda ao processo

Gume

O CRM que acompanha o seu processo comercial, pronto para agentes de IA via MCP, com API pública e plano gratuito para começar.

Papel
Autor · produto, arquitetura, engenharia e operação
Período
ago a out 2026
Stack
  • TypeScript
  • Next.js
  • Fastify
  • PostgreSQL
  • Redis
  • S3
  • Drizzle
  • MCP
  • Turborepo
Link
gume.app.br(abre em nova aba)
commits em 8 semanas
621
ADRs (registros de decisão)
48
arquivos de teste
531
operações na API pública
170

Números medidos no repositório.

Ambiente de demonstração, com dados fictícios.

Contexto e problema

Time comercial passa o dia dentro do CRM, mas quase nunca dentro de um CRM que acompanha o processo dele. Campo novo vira chamado, funil novo vira projeto, e automação é caixa-preta: ninguém sabe o que ela vai fazer até ela fazer, em produção.

O Gume é o meu produto para esse problema, um CRM SaaS que estou levando ao mercado em gume.app.br. A proposta é um CRM em português para equipes que querem configurar o próprio processo: funil em kanban, detalhe e histórico de cada negócio, funis e campos próprios, automações visuais, API documentada, importação de CSV, permissões por pessoa e histórico de alterações. A interface sai em português, inglês e espanhol, e o cadastro aceita conta Google ou Microsoft. Existe um plano gratuito com limites (3 usuários, 5.000 registros, 3 automações ativas, 500 execuções por mês e 500 MB). Os planos pagos ainda estão em definição.

O código é fechado e o produto é comercial. Por isso este case descreve a engenharia em texto, sem trechos de código. Os blocos “Detalhes técnicos” abrem com um clique, para quem quiser ver como cada parte funciona por dentro.

Restrições que eu me impus

  • Vários clientes no mesmo banco, e nenhum caminho de leitura ou escrita pode cruzar workspaces. A autorização acontece sempre no servidor; a interface só reflete o que o servidor decidiu.
  • PostgreSQL é a única fonte da verdade. Redis guarda só limite de requisições e coordenação de curta duração, e perder o Redis não pode apagar nada.
  • Configurar não pode exigir migração. Criar um campo é escrita de metadado, nunca ALTER TABLE.
  • Automação tem que ser previsível. O usuário precisa ver o que um fluxo faria antes de publicar.
  • O produto inteiro precisa caber num servidor só e barato, com backup verificado e monitoramento, enquanto não houver receita que justifique mais.
  • Soft delete por padrão, dinheiro em NUMERIC e um histórico de alterações gravado junto com cada escrita.

Decisões que definiram o produto

Arquitetura

NavegadorUsuário do CRMEdge proxyRoteia /api e webWebNext.js · ReactAgente de IAMCP · chave com escopoAPIFastify · OpenAPIRedisSessão · rate limitWorkerConsome o outboxPostgreSQLEstado + outboxStorage S3Arquivos privadosIntegraçõesWebhooks, agenda, e-mailReprocessoEvento segue no outbox

Toda escrita passa pela API e grava estado, auditoria e evento de outbox na mesma transação do PostgreSQL. Só depois do commit o worker consome o evento e aciona automações, webhooks e integrações. Agentes de IA usam o MCP, que fala com a mesma API REST e nunca com o banco.

Em produção, um proxy de borda roteia /api/v1/* direto para a API e o resto para o Next.js. O worker consome o outbox, executa automações e roda as rotinas de retenção. O servidor MCP roda na máquina de quem usa, sem acesso ao banco nem regra de negócio: ele traduz ferramentas em chamadas à mesma API REST, com uma chave de serviço com escopo.

Detalhes técnicos: o SaaS inteiro numa VPS de 6 vCPU e 12 GiB7 processos · backup diário

A produção é um único host Linux com 6 vCPU e 12 GiB de RAM rodando sete processos em contêineres: borda, web, API, worker, PostgreSQL, Redis e MinIO. Só a borda publica portas (80 e 443). O resto fica numa rede privada, e o PostgreSQL escuta apenas no loopback, para o backup.

O orçamento de memória está escrito num contrato de topologia que o CI confere: 3 GiB para o PostgreSQL, 1,5 GiB para a API, 768 MiB para o web, 512 MiB para worker e MinIO, 384 MiB para o Redis e 128 MiB para a borda. Sobram cerca de 5,25 GiB para sistema operacional, runtime de contêiner e cache de disco. O Redis roda com noeviction: se a memória acabar, ele recusa a escrita em vez de descartar um contador de limite em silêncio.

O backup roda todo dia: pg_dump, criptografia com age e envio para um bucket S3 versionado. A identidade que produz o backup só consegue gravar; quem apaga cópias antigas é outra conta, com outra credencial, e a chave privada de restauração fica fora do servidor. Cada execução grava um manifesto com hash, tamanho e a última migração aplicada, e só avisa o monitor de sucesso depois de verificar o snapshot. Se o aviso não chega em 26 horas, recebo um alerta. Um ensaio de restauração completo (dump, criptografia, upload, download da versão exata, decriptação e restore num banco vazio) roda no CI.

O deploy é um script que guarda um dump antes de cada release (mantido por 90 dias) e tem caminho de rollback. Para saúde do host, um agente pequeno lê /proc e o Docker a cada 60 segundos e manda um snapshot para a própria API, guardado por 30 dias no PostgreSQL. Escolhi isso no lugar de Prometheus e Grafana porque um stack de observabilidade completo custaria mais memória que o próprio worker.

O que o produto entrega por dentro

Modelo de dados e campos customizados

Leads, negócios, empresas, contatos, tarefas e produtos são objetos nativos, com colunas especializadas. Por cima deles, cada workspace cria campos próprios sem migração.

Detalhes técnicos: valores tipados, chaves compostas e índices por workspaceADR-0006 · ADR-0015

Uma definição de campo tem UUID imutável, tipo de dono (lead, negócio, empresa ou contato), chave de máquina, tipo de valor e cardinalidade única ou múltipla. Os valores ficam em linhas ordenadas numa tabela compartilhada. Cada linha repete o tipo de dono, o tipo de valor e a cardinalidade da definição sob uma chave estrangeira composta, e um check exige que só a coluna tipada correspondente esteja preenchida. O banco recusa um texto num campo de número mesmo que a aplicação erre.

Tipo e cardinalidade não mudam depois de criados. Mudança incompatível vira campo novo mais uma migração de dados explícita, para que linhas antigas nunca sejam reinterpretadas. Apagar uma definição é lógico e preserva os valores. Referências a outro registro ou a um membro do workspace são polimórficas, então um adaptador registrado por entidade trava e valida o alvo na mesma transação; referência para outro workspace falha.

Filtros exatos e por faixa usam índices B-tree compartilhados que começam por workspace, definição e valor tipado, sem índice dinâmico por cliente. Busca aproximada não é prometida até um plano de consulta medido justificar o custo. Há limites rígidos por campo, por valor e por requisição, porque campo múltiplo multiplica linhas e o tamanho do histórico.

O isolamento entre clientes segue a mesma ideia: as tabelas carregam workspace_id, e as chaves estrangeiras compostas incluem o workspace, de modo que essas relações não conseguem apontar para dado de outro cliente nem por erro de aplicação. Por cima disso, serviços e repositórios conferem o workspace de novo depois da autorização da rota.

Automações e simulação

O canvas visual monta fluxos com gatilho, condições, ações no CRM, espera e chamada HTTP. Antes de publicar, o usuário roda uma simulação que mostra o que cada passo faria.

Detalhes técnicos: versões, esperas duráveis e simulação com estado-sombradraft/published · fixtures

Cada automação tem rascunho e versões publicadas, e uma execução fica presa à versão que estava publicada quando o gatilho disparou. Editar o fluxo não muda o que já está rodando.

Espera é estado persistido. A execução vai para WAITING com um horário de retomada, e o agendador de cadências acorda as que venceram com FOR UPDATE SKIP LOCKED, para dividir o trabalho sem duas instâncias pegarem a mesma linha. Espera em dias úteis fixa a versão do calendário no momento do agendamento. Na retomada, a condição de cancelamento é avaliada contra o registro atual, então um negócio que já foi ganho não recebe o follow-up que estava agendado.

A simulação recebe só portas de leitura do workspace, um estado-sombra em memória que as ações simuladas alteram e um resolvedor de fixtures HTTP. Um passo HTTP só “funciona” se a requisição trouxer a fixture exata daquele nó; sem ela, a simulação devolve uma negação antes de qualquer adaptador de rede existir no caminho. Esperas viram tempo virtual. O resultado é um trace versionado com o registro antes e depois, e nada fica gravado: duas simulações iguais dão o mesmo resultado lógico e não deixam rastro no banco.

O plano entra aqui também. Execuções contam num contador mensal por workspace; acima do limite, a execução não é criada, o gatilho fica marcado como barrado pelo plano e não é reprocessado, e um único evento de auditoria por mês registra que o teto foi atingido.

Eventos, outbox e worker

Webhooks de saída, automações e integrações saem do outbox, nunca da requisição do usuário.

Detalhes técnicos: claims idempotentes, leases e webhooks assinadosoutbox · at-least-once

O worker lê eventos já commitados e processa cada um numa transação que faz duas coisas juntas: insere o claim do evento (com conflito ignorado) e grava o efeito. Se o processo morrer no meio, não sobra claim sem efeito nem efeito sem claim. Execuções de automação usam lease com token e prazo; um worker que perdeu o lease não consegue gravar o resultado de um passo. O loop de polling volta imediatamente quando a última passada encontrou trabalho, para drenar fila acumulada sem esperar o intervalo ocioso.

Webhook de saída congela o corpo canônico do evento, assina cada tentativa com HMAC, tem identidade estável para o receptor deduplicar, retry com backoff, fila de mortos e reenvio manual pelo administrador. A chamada de rede acontece num consumidor próprio, fora do consumidor interno do outbox, que só mexe no PostgreSQL. Webhook de entrada valida a assinatura do endpoint, grava um recibo idempotente e o evento no outbox e só então responde 202; a execução é criada depois, de forma assíncrona.

O mesmo worker roda de hora em hora a rotina de retenção de dados pessoais, em lotes de 1.000 linhas e usando o relógio do banco: corpos de webhook e de e-mail, linhas brutas de importação e saídas de passos de automação são anulados ou redigidos em janelas que vão de 7 dias a 12 meses, e trilhas antigas são apagadas.

Acesso: login, permissões e chaves de API

Cada pessoa entra com e-mail e senha, Google ou Microsoft. O administrador define o que cada membro pode ver e alterar, e integrações usam chaves com escopo.

Detalhes técnicos: sessão no servidor, perfis que falham fechados e admissão em RedisArgon2id · HMAC · 5 buckets

A sessão é um token opaco num cookie HttpOnly; o banco guarda só um digest com chave, e cada requisição resolve usuário e membership no PostgreSQL. Senha usa Argon2id. Mutações do navegador exigem Origin exata e token CSRF. Conta Google é identificada pelo sub verificado e conta Microsoft por tenant mais sub; colisão de e-mail na Microsoft falha em vez de vincular contas. Um hub único faz login e escolha de ambiente, e a passagem entre hosts usa um handoff de uso único guardado no Redis; o host de destino cria a própria sessão no PostgreSQL.

Os papéis são administrador e membro. Membros podem receber um perfil nomeado com capacidades de leitura, criação, edição e exclusão separadas para lead, negócio, empresa e contato (16 combinações). Perfil vazio, desativado ou apagado concede nada, nunca o padrão. As capacidades são resolvidas na mesma transação da requisição, sem cache, então tirar um acesso vale na próxima chamada.

Chaves de API são opacas, aparecem uma vez, têm validade obrigatória e são verificadas por HMAC com um keyring de pepper que permite rotação. Só um administrador logado por cookie cria, gira ou revoga chaves; uma chave não administra outra. Requisição com cookie e Bearer ao mesmo tempo é recusada. Todo endpoint precisa estar classificado no registro de rotas, com capacidade humana e escopo de serviço, ou a API não sobe; o OpenAPI é gerado desse registro.

O limite de requisições por chave é uma janela fixa em Redis. Um script atômico confere e incrementa cinco contadores de uma vez (versão da chave, principal, workspace, rota e classe de tráfego) ou não incrementa nenhum, e nada é devolvido depois de admitido. Se o Redis falhar, só o tráfego de chaves recebe 503; quem está no navegador continua lendo.

API e servidor MCP

A API é a mesma superfície que o app e os agentes usam, documentada em OpenAPI 3 e publicada com referência interativa, llms.txt e exportação “copiar para IA”. A especificação pública tem 170 operações em 136 rotas. Algumas escolhas que tornam integrações difíceis de quebrar:

  • Mudar um negócio de etapa tem endpoint próprio (POST .../stage-transitions). O update comum não aceita etapa, e o histórico de movimentação é imutável.
  • Updates aceitam expected_updated_at. Se alguém editou antes, a escrita falha em vez de sobrescrever em silêncio.
  • Paginação por cursor opaco e erros com código estável, para o cliente tratar por código e nunca por texto.
  • Rotas que só fazem sentido para a sessão do app ficam fora da documentação pública, de propósito.
Detalhes técnicos: 58 ferramentas MCP, cada uma presa a uma operação RESTstdio · cliente não confiável

O servidor MCP é um processo Node.js 24 com o SDK oficial, falando por stdio. Ele se autentica com uma chave de serviço do workspace e se comporta como um cliente REST não confiável: não importa o pacote de banco, não abre conexão com o PostgreSQL e não tem regra de negócio. Workspace e autor da ação vêm sempre da autenticação, nunca do que o agente mandou.

São 58 ferramentas em seis grupos: inspeção do workspace, funis e etapas, leads, negócios, campos customizados (seções, definições e opções) e automações com suas execuções. Cada ferramenta aponta para uma operação REST canônica, declara o escopo exigido e se lê ou altera dados. Não existe ferramenta genérica de HTTP, SQL ou execução de código.

Executar e simular uma automação são ferramentas diferentes, e a simulação é classificada como leitura. Um agente com chave só de leitura pode testar fluxos à vontade sem efeito real. Mutações não são repetidas às cegas, o stdout fica reservado para o protocolo e o diagnóstico vai para o stderr já sem credenciais, prompts ou corpos de resposta. Interpretação de linguagem, planejamento e aprovação do operador ficam com o agente.

Meu papel

Fiz tudo: visão de produto, arquitetura, código, testes, deploy e operação. Os 621 commits do repositório, de 20 de agosto a 18 de outubro de 2026, são meus. O desenvolvimento é guiado por especificações e ADRs e acelerado por agentes de IA: cada decisão relevante fica registrada antes de virar código, o que manteve 48 decisões coerentes em 8 semanas. São 96 migrações SQL e 10 workflows de CI, a maioria em runners próprios.

Resultado

  • Leads, negócios, empresas, contatos, tarefas e produtos nativos, campos customizados, múltiplos funis com etapas abertas, ganhas e perdidas, kanban, tabela e dashboards salvos.
  • Automação visual persistente com canvas, simulação segura, trilha de execução, esperas duráveis e cadências em dias úteis.
  • API REST com OpenAPI, chaves de serviço com escopo, webhooks de entrada e saída e servidor MCP para agentes de IA.
  • Operação de SaaS: planos com limites por workspace, backups criptografados e verificados, retenção de dados pessoais executada pelo worker, perfis de acesso granulares e login com Google e Microsoft.
Canvas de automação: gatilho, etapa e conclusão.

O que eu faria diferente

  • Gerar o contrato entre canvas e kernel em vez de manter cópias sincronizadas à mão entre o app web e o pacote do motor.
  • Ter disciplina de numeração de ADRs desde o primeiro dia. A velocidade gerou números duplicados; é pouco, mas é o tipo de ruído que um processo guiado por documentos não deveria ter.

Quer algo parecido no seu time?

Falar comigo