CRM multiempresas com compliance de mensageria e conversão offline para Google Ads e Meta.
Três empresas do Fort Grupo, em Januária/MG, cada uma numa instância com banco de dados próprio — não é separação por permissão, é arquivo diferente — com consulta federada entre elas e uma central que consolida os leads de todas as fontes.
🔗 Sistema no ar: fortcrm.fattech.com.br
Node ≥ 23.4 · zero dependências · 139 testes · nenhum passo de build
O problema mais caro de qualquer negócio de recorrência não é guardar cadastro — qualquer planilha faz isso. É o cliente que precisava voltar, ninguém lembrou de chamar, e ele foi no concorrente.
O FORT-CRM monta sozinho a fila de quem falar hoje, com a mensagem já escrita e o motivo à vista — e recusa o envio quando não pode enviar. É essa recusa que protege o número de WhatsApp da empresa.
E fecha o outro lado do ciclo: devolve ao Google e à Meta o que aconteceu depois do clique, para que a campanha que de fato traz gente deixe de parecer que não traz.
| Empresa | Segmento | O que tem de próprio |
|---|---|---|
| Minas Peças | Injeção diesel — Bosch Car Service | frota, ordens de serviço, laudo de bancada |
| Fazenda Agrofort | Queijo artesanal | pedidos, recompra, clube de assinatura |
| Fort Tintas | Tintas e vernizes | pedidos, pintor parceiro, obra |
Não tem npm install. Não tem build. Não tem contêiner obrigatório.
node server.mjsAbre em http://127.0.0.1:4501. A carga de demonstração roda sozinha na primeira
subida — três instâncias, ~38 clientes, histórico de doze meses.
Node ≥ 23.4 é obrigatório. O sistema usa node:sqlite, que só existe sem
flag a partir dessa versão. No Node 22 ele sobe e morre no primeiro import.
node src/selftest.mjs # 45 testes — CRM de uma empresa
node src/selftest-fed.mjs # 94 testes — federação, atribuição, conversão, CTWASenha demo no ambiente local. Em produção a senha inicial é gerada pelo
instalador e nunca é demo.
| Papel | Enxerga | |
|---|---|---|
diretoria@fortgrupo.com.br |
direção | as três empresas |
balcao@minaspecas.com.br |
operação | Minas Peças |
adenilde@agrofort.com.br |
operação | Agrofort |
loja@forttintas.com.br |
operação | Fort Tintas |
root@fortgrupo.com.br |
soberano | acesso técnico, as três |
comercial@fortgrupo.com.br |
gestor | as três |
consulta@fortgrupo.com.br |
leitura | as três, sem escrever nada |
O sistema abre no Início, e não num painel de números. Cada botão é nomeado pelo que a pessoa quer fazer — "Falar com clientes hoje", não "Régua de contato" — e carrega estado vivo: 7 pessoas esperando, R$ 14.300 em negociação. O número ensina o que a tela é sem uma linha de explicação.
Detalhe do balcão: o cartão de clientes tem busca embutida. Com o cliente na frente, digite o nome e tecle Enter.
Ctrl+K, /, ou o campo na lateral. Acha cliente, veículo, ordem de serviço, pedido, oportunidade, item de catálogo e tela — por nome, telefone, placa ou número de OS.
Sem acento e sem pontuação: antonio acha Antônio, (38) 99811-2233 acha o
mesmo que 5538998112233. As telas atendem por apelido — cobrança leva à
régua, meta leva a Conversões.
A busca não atravessa empresas. Cada uma tem banco próprio. Quando não acha, ela diz onde procurou e oferece procurar nas outras a que você tem acesso — a travessia é explícita, e troca a empresa ativa de verdade.
Lead de Click-to-WhatsApp não tem UTM — o anúncio abre a conversa direto, sem
navegador e sem página. A tela de origem agrupava por utm_campaign, então todo
lead de anúncio caía em sem_campanha: o canal onde há verba era o único sem
resposta.
Agora o ad_id vira nome de campanha, conjunto e anúncio, lidos do Gerenciador
da Meta e guardados por sete dias. Sem token, sem rede ou com erro, a tela mostra
o número do anúncio e diz por quê — nunca um nome inventado.
Mover para Ganho pede o valor (com vírgula, 1.650,00) e o número do pedido.
Sem valor, a conversão era recusada bem depois, dentro do processamento, onde
ninguém lê: o evento mais valioso do funil era o mais fácil de perder em
silêncio.
Depois que a conversão sai de verdade, o cartão trava. A Meta já contou; arrastar de volta só faria o CRM discordar do que ela registrou.
Na régua, "esse eu falo semana que vem" virou botão. O adiamento é por (cliente, gatilho) — adiar a revisão de um caminhão não silencia a cobrança de orçamento do mesmo cliente — e nunca é silencioso: os adiados aparecem numa faixa no topo da fila, com a data de volta e um botão para trazer de volta. Se a fila esvaziar só por causa deles, a tela diz isso, e não "não há trabalho".
Primeiro item do menu: Manual do sistema. 18 telas explicadas uma a uma, 23 verbetes de glossário, 12 receitas de "como faço para…" e uma lista honesta do que o sistema ainda não faz. Cada seção tem um botão que abre a tela de verdade.
| Documento | O que cobre |
|---|---|
| Arquitetura | Federação, instâncias, central, escopo e auditoria |
| Régua e compliance | Os 17 gatilhos, as 9 checagens, idempotência |
| Campanhas | O nome por trás do ad_id, e por que o sistema nunca inventa um |
| Credenciais | Token da Meta por empresa, cifrado em repouso |
| Atribuição e conversões | Do clique no anúncio à conversão devolvida |
| Click-to-WhatsApp | ctwa_clid, webhook assinado, Business Messaging |
| Canais de entrada | Catálogo por empresa, porta pública, cobertura |
| Campos personalizados | Registro de propriedades, JSON validado, por empresa |
| Interface | Cinco temas, versão mobile, decisões de usabilidade |
| API | Referência das rotas |
| Deploy | Contêiner isolado, túnel, backup, manutenção |
| Decisões e armadilhas | Os bugs encontrados e por que a solução é essa |
| Roteiro de demonstração | 12 minutos, passo a passo |
| Relatório técnico | Levantamento completo: números, arquitetura, funcionalidades e defeitos encontrados |
| Roadmap | O que faz hoje e as 50 próximas implementações |
Cada empresa é um arquivo SQLite separado. Quem atende o balcão da oficina não
enxerga um pedido de queijo porque aquele dado não está no sistema que ele
abriu — não porque uma cláusula where o escondeu.
O escopo por empresa_id continua existindo como segunda barreira, e permite que
uma instância hospede mais de uma empresa quando fizer sentido.
A central consulta cada instância e junta em memória. É mais trabalho do que
anexar bancos, e é deliberado: o desenho previsto é cada empresa poder virar
outra máquina. ATTACH só funciona no mesmo sistema de arquivos.
Instância fora do ar não derruba a consulta — ela some da lista e as outras seguem. Numa federação, indisponibilidade de uma é evento normal, não erro.
Ela consolida para leitura. Toda escrita acontece na instância da empresa.
Nove checagens ordenadas — consentimento, descadastro, janela de 24 h, cooldown do gatilho, blocklist, teto do canal, canal conectado. A primeira que falha bloqueia e diz o motivo na tela.
O texto que o operador editar passa pela mesma checagem no servidor. Aceitar o que o navegador mandou daria a ele um jeito de furar a própria regra da empresa sem perceber.
Um timeout não diz se a mensagem saiu. O estado vira desconhecido e exige
olho humano. Uma mensagem atrasada custa um dia; uma duplicada custa a confiança
do cliente — e, na conversão, uma decisão de verba tomada em cima de número
inflado.
server.mjs HTTP, roteamento, arquivos estáticos
src/
federacao.mjs catálogo de instâncias, consulta federada
db.mjs SQLite, escopo por empresa, cadeia de auditoria
schema.mjs 14 tabelas do CRM
schema-extra.mjs event_log, atribuições, conversões, webhooks, chaves
migracoes.mjs colunas novas em base existente
compliance.mjs as 9 checagens — puro, sem I/O
regua.mjs 17 gatilhos e renderização de mensagem
atribuicao.mjs parâmetros de clique, normalização, hash SHA-256
conversoes.mjs payloads do Google Ads e da Meta CAPI
conversoes-servico.mjs fila, avaliação e despacho
ctwa.mjs Click-to-WhatsApp: webhook, HMAC, Business Messaging
canais.mjs catálogo de canais de entrada por empresa
propriedades.mjs registro de campos: sistema + personalizados
central.mjs consolidação e chaves de captação
senha.mjs scrypt, migração automática
limite.mjs teto de requisições por origem
api.mjs ~45 rotas
seed.mjs carga de demonstração
selftest*.mjs 139 testes
web/
app.js SPA, sem framework
ui.js ícones SVG e temas
mobile.js navegação de polegar, tabela→cartão
styles.css sistema visual
temas.css cinco temas
mobile.css dois pontos de corte: 820 e 640
manual.js o manual dentro do sistema
deploy/ Dockerfile, compose, túnel, manutenção
docs/ esta documentação
Escrito aqui para que ninguém prometa ao cliente o que ele ainda não entrega.
- Nenhuma mensagem chega a número real.
DEMO_MODEestá ligado; os disparos são registrados e auditados como simulação. Ligar o envio de verdade exige um adaptador de canal que ainda não existe. - Nenhuma conversão sai para o Google ou para a Meta. O payload é montado, avaliado e gravado — é ele que mostra numa reunião exatamente o que iria, e é ele que denuncia um campo faltando antes de a conta estar ligada.
- Não há MFA, bloqueio por tentativas nem revogação de sessão. Num endereço público permanente, Cloudflare Access na frente do hostname é obrigatório.
- A régua é calculada quando alguém abre a tela. Em produção, roda de madrugada por timer.
- Não dá para cadastrar veículo, ordem de serviço ou item de catálogo pela interface. Só clientes e oportunidades nascem por aqui.
- A base é fictícia. Nenhum dado real de cliente está aqui.
Protótipo comercial do Fort Grupo, construído dentro da doutrina Palantyr. Código sem dependências de terceiros — o que está aqui é auditável linha a linha.