Plannus CRM · set 2026 a atual
Plannus CRM
CRM em produção auditado e reconstruído: segurança, testes, arquitetura e migração para Cloud Run, com automação de WhatsApp e assistente de IA de vendas.
33
falhas encontradas na auditoria, 7 críticas
11 → 487
casos de teste na API
10 dias
para fechar todos os bloqueadores
Resumo do que fiz
- Auditoria completa do sistema antes de qualquer mudança: 33 falhas registradas com evidência, 7 delas críticas.
- Bloqueadores fechados em 10 dias, com cada lição virando guarda executável que reprova o build.
- Testes da API de 11 para 487 casos (121 ponta a ponta contra Postgres real) e do app de 3 para 482.
- Migração de VM com deploy manual para Cloud Run, com infraestrutura como código.
- 258 rotas padronizadas com contrato OpenAPI e índices do banco de 26 para 53.
Tecnologias
- Arquitetura refatoradaentregue
- CI/CD estruturadoentregue
- Segurança e permissões corrigidasentregue
- Relatórios com dados validadosentregue
- Base de dados pronta para IAem curso
GCP · WhatsApp API (Meta) · IA de vendas · CI/CD
A virada
O que mudou no projeto
O Plannus chegou até mim em setembro de 2026 como um CRM em produção, com clientes usando, e uma API de 36 mil linhas coberta por 4 arquivos de teste. Antes de qualquer mudança, auditei o sistema inteiro e registrei 33 falhas, 7 delas críticas, cada uma com evidência. Em 10 dias de rodadas de correção, os bloqueadores foram fechados, cada lição virou uma guarda executável que reprova o build, e o sistema saiu de uma VM com deploy manual pro Cloud Run, com toda a infraestrutura em código. Os números abaixo foram apurados nos próprios repositórios do projeto e na auditoria registrada neles.
33
Falhas encontradas pela auditoria antes de qualquer mudança, 7 delas críticas, cada uma com evidência.
4 → 72
Arquivos de teste na API: de 11 casos de teste a 487, com e2e contra Postgres real.
10 dias
Da primeira à última rodada de correção, cada lote registrado com o que mudou e por quê.
0
Arquivos da API acima do teto de 500 linhas hoje. O maior tinha 2.698.
Arquitetura
A infraestrutura antes e depois
Cada linha da coluna da esquerda é o estado que encontrei. À direita, o que ficou no lugar.
Antes
Depois
Problema e solução
Ponto a ponto, o que estava errado e o que fiz
Cada par mostra o problema encontrado e a correção aplicada.
O que encontrei
Uma senha-mestra abria qualquer conta
O login aceitava uma senha fixa no lugar do hash de qualquer usuário, inclusive das contas que administram a plataforma inteira, sem registro nem alerta.
O que fiz
Senha-mestra removida
O login voltou a comparar só o hash, e o caminho ganhou teste: a senha antiga responde 401 em qualquer conta.
O que encontrei
Cinco gateways de tempo real sem autenticação
Os WebSockets aceitavam qualquer origem e entravam na sala do workspace que o cliente pedisse, sem validar token nem vínculo.
O que fiz
WebSockets autenticados no handshake
Um gateway base valida a sessão, resolve o workspace pelo token e ignora o que o cliente envia. Os cinco herdam dele.
O que encontrei
Segredo do JWT com fallback público
Em quatro pontos, a ausência da variável de ambiente caía num literal conhecido. O boot seguia normal, assinando tokens que qualquer pessoa conseguiria forjar.
O que fiz
Ambiente validado no boot
Um schema zod exige os segredos em produção, rejeita o valor de exemplo e derruba o boot listando tudo que falta, de uma vez.
O que encontrei
CORS aberto e sem cabeçalhos de segurança
A API refletia qualquer origem com credenciais, e a variável que listava as origens não era lida em lugar nenhum. Sem helmet, sem HSTS, sem nosniff.
O que fiz
CORS por allowlist e helmet
As origens vêm da configuração, e os cabeçalhos de segurança vão em toda resposta. O X-Powered-By saiu.
O que encontrei
Nenhum limite de requisições
Login, cadastro e recuperação de senha aceitavam tentativas ilimitadas, sem bloqueio nem backoff.
O que fiz
Rate limit por rota
Limite global e limites dedicados nas rotas de autenticação, no assistente, no envio de chat e nos webhooks, contando o IP real atrás do balanceador.
O que encontrei
Token de 7 dias sem refresh nem revogação
Um único token valia uma semana. Trocar a senha, bloquear ou arquivar o membro não invalidava nada já emitido.
O que fiz
Access de 15 minutos e refresh rotativo
Refresh opaco guardado como hash, reuso derruba todas as sessões, logout e troca de senha revogam. Os dois tokens em cookie httpOnly.
O que encontrei
Quatro rotas públicas serviam arquivos de qualquer cliente
Anexos de negócios, mídias de conversas e avatares saíam sem checar sessão nem workspace. Bastava ter o identificador.
O que fiz
Arquivos só com sessão e workspace válidos
As rotas exigem autenticação e checam o vínculo antes de servir. Identificador de outro workspace responde 404 ou 403.
O que encontrei
Schema do banco mudado pelo ORM, com 53 migrations inúteis
O synchronize reescrevia o schema a partir das entidades, e nenhuma das migrations criava a tabela base. Era impossível subir um ambiente novo a partir do repositório.
O que fiz
Baseline e synchronize desligado em todo ambiente
Uma migration de baseline gerada das entidades cria o schema num banco vazio. As antigas foram arquivadas, e o CI sobe o banco do zero antes dos e2e.
O que encontrei
Uma transação em todo o código
O cadastro gravava usuário, workspace e vínculo em operações separadas. Uma falha no meio deixava um usuário órfão, que não conseguia nem logar nem se recadastrar.
O que fiz
Fluxos multi-escrita em transação
Criar e mover negócio gravam o histórico na mesma transação, e o webhook escreve sob trava de linha. De uma transação pra nove.
O que encontrei
Erro do banco ia direto pro cliente
Sem filtro de exceção global, um erro do driver escapava como 500 com nome de tabela, coluna e constraint, e cada rota devolvia um formato diferente.
O que fiz
Envelope e filtro de exceção globais
Toda resposta tem success, message e data. Todo erro tem código estável e detalhes por campo, e nenhuma mensagem de driver chega ao cliente.
O que encontrei
Entidades devolvidas cruas
Os 29 controllers devolviam a entidade inteira, com relações. A lista de usuários do workspace entregava o hash de senha de todos os membros.
O que fiz
Serialização com exclusão garantida
Campos sensíveis marcados com @Exclude e um interceptor próprio que quebra ciclos sem perder a exclusão. Há teste travando o hash de senha fora da resposta.
O que encontrei
O maior service tinha 2.698 linhas e 64 métodos
Acumulava CRUD, kanban, merge de duplicados, upload, notas, envio de WhatsApp e automações, e era o ponto de acoplamento de quase todo o domínio. Havia 44 métodos com mais de 80 linhas.
O que fiz
Services fatiados por responsabilidade
Os seis maiores viraram 44 arquivos, nenhum acima de 400 linhas, sempre começando pelos testes. O teto de 500 linhas é uma catraca que só desce, e o mapa de exceções está vazio.
O que encontrei
Escopo de tenant lembrado à mão em 83 pontos
O padrão buscava o registro por id e só depois comparava o workspace. Uma omissão num método novo viraria vazamento silencioso.
O que fiz
Escopo de tenant verificado por guarda
Toda consulta a repositório precisa carregar o workspace ou constar numa lista com motivo escrito. A guarda reprova o build e afirma quantos arquivos leu.
O que encontrei
4 arquivos de teste pra 36 mil linhas
Nada cobria autenticação, permissões, billing ou o motor de automação. Refatorar sem rede de segurança era aposta.
O que fiz
487 casos de teste, 121 deles e2e
Segurança, relatórios e ciclo de vida do negócio rodam contra Postgres real no CI, e cada asserção é conferida por mutação.
O que encontrei
Sem CI, Dockerfile ou hook
Lint, build e teste dependiam de disciplina manual, e o lint acusava 2.561 problemas.
O que fiz
CI nos dois repositórios e hooks locais
Lint sem warning, tipos, testes, e2e, build, auditoria de dependências, Docker e varredura de segredos. Pré-commit e pré-push rodam o mesmo lint em container.
O que encontrei
App sem camada de design nem camada de serviço
Zero variáveis CSS, 951 cores em hex nos estilos, treze componentes chamando a API direto e dez cópias de formatCurrency que discordavam entre si.
O que fiz
Design system com tokens e services com contrato
219 variáveis de token, zero cor crua, um formatador só. As 28 rotas do painel carregam sob demanda, e o bundle inicial caiu de 404 pra 243 kB.
Entrega
Os números apurados no repositório
Nenhum número aqui é estimativa: todos saíram do próprio repositório do projeto.
487
Casos de teste na API: 366 unitários e 121 e2e contra Postgres real, de 11.
482
Casos de teste no app, de 3 num arquivo gerado pelo CLI.
258
Rotas que a API expõe hoje, sob um envelope único e contrato OpenAPI exportado.
53
Índices no banco, de 26. As 12 entidades de tenant sem índice ganharam o seu.
6
Guardas executáveis na API que reprovam o build, mais 5 verificações estáticas no app.
11
ADRs registrando as decisões de arquitetura, 5 na API e 6 no app.
Arquivos de teste na API
arquivos de teste
De 11 casos de teste pra 487, 121 deles e2e contra Postgres real.
Casos de teste no app
casos de teste
De um arquivo gerado pelo CLI pra 84 arquivos de spec.
Maior arquivo da API
linhas
Teto de 500 linhas imposto por guarda. Nenhum arquivo acima dele, e o mapa de exceções está vazio.
Cobertura de linhas da API
Linhas cobertas
Medição registrada em 05/10/2026 com os e2e contra Postgres real. Os unitários sozinhos cobrem 13,8%.
Achados da auditoria, por severidade
Frentes de trabalho
Tudo que foi feito, por área
A entrega dividida nas frentes que ela realmente teve.
Segurança e acesso
Da senha-mestra à sessão em cookie, o que protegia menos do que devia e como ficou.
Senha-mestra removida
O login compara só o hash. A senha antiga responde 401 em qualquer conta, e há teste travando isso.
WebSockets autenticados
Sessão validada no handshake e workspace resolvido pelo token, nos cinco gateways.
Sessão inteira em cookie httpOnly
Prefixos __Host- e __Secure-, Secure, SameSite=Strict. O JavaScript da página não enxerga token nenhum.
Refresh rotativo com detecção de reuso
Guardado como hash. Reutilizar um refresh antigo derruba todas as sessões e fica na trilha de auditoria.
Ambiente validado no boot
Segredo faltando ou valor de exemplo derruba o boot com a lista completa do que falta.
Superfície pública inventariada
Toda rota @Public() está numa lista com motivo escrito e mecanismo próprio de proteção. A guarda reprova a que não estiver.
Permissões sem atalho
Cargo não concede nada e mapa vazio significa exatamente o papel. Antes, mapa vazio concedia tudo.
Trilha de auditoria
Login falho, reuso de refresh e redefinição de senha gravados em audit_logs.
Confiabilidade em várias instâncias
O que se perdia a cada deploy numa VM, e como passou a sobreviver a reinício e a escala a zero.
Outbox transacional
Webhook, cópia de mídia e automação gravam o trabalho antes de responder. A execução sai por fora, com varredor de 30 s como rede de segurança.
Passo de automação não se perde
Reivindica, executa e só então apaga, com FOR UPDATE SKIP LOCKED. Antes era apaga e executa.
Tarefa de boot numa instância só
Advisory lock do Postgres evita que cada cold start repita seeds e re-registro de webhooks.
Escrita concorrente sob trava de linha
Dois eventos do mesmo workspace não sobrescrevem mais um ao outro: transação curta, pessimistic_write, nada de rede dentro.
Health checks separados
/health/live e /health/ready, este conferindo o banco, com shutdown hooks ativos.
Logs estruturados
Pino em JSON, com correlação de requisição e segredos mascarados no caminho da URL.
Banco e arquitetura
Como o schema passou a ter baseline, as chaves viraram chaves de verdade e os arquivos gigantes foram fatiados.
Baseline de migrations
Gerada das entidades, cria o schema num banco vazio. synchronize: false em todo ambiente.
Chaves estrangeiras de verdade
Colunas que apontavam pra outra entidade eram varchar em 18 casos, e 18 tabelas não tinham FK. Agora uuid uniforme, com FK.
Seis services-deus fatiados
Viraram 44 arquivos, nenhum acima de 400 linhas, sempre começando pelos testes.
Teto de 500 linhas como catraca
A guarda só deixa o teto descer, e o mapa de exceções está vazio.
Escopo de tenant por guarda
Consulta sem workspace só passa com motivo escrito. A guarda afirma quantos arquivos leu.
Contrato OpenAPI conferido no CI do app
Cada URL que o front monta precisa existir no snapshot exportado da API.
Front-end e design system
Do Angular 18 ao 21, e da cor escrita à mão ao token: o que mudou pra quem usa o sistema todo dia.
Angular 18 → 21
Um major por vez, com o portão verde entre eles. Cada versão nova achou um bug real.
Design system com tokens
219 variáveis de token no lugar de 951 cores em hex nos estilos. Cor crua reprova o build.
Componente desenha, service busca
Treze componentes chamavam a API direto; agora um. Dez cópias de formatCurrency viraram um formatador só.
Zero componentes acima de 400 linhas
Eram 21, somando 14.851 linhas acima da régua.
CSP sem script inline
script-src 'self', e uma guarda no build reprova handler inline e folha presa em media=print.
URLs em português
Cerca de 50 rotas em inglês viraram interface em português, com redirecionamentos.
Idioma imposto por guarda nos dois lados
453 identificadores em português no app e 207 na API saíram. A regra reprova o build.
Bundle inicial menor
De 404 pra 243 kB, com as 28 rotas do painel sob demanda e ECharts fora do chunk inicial.
Qualidade que se sustenta
Guardas, CI, hooks e documentação: o que impede a reincidência depois que eu saio de cena.
6 guardas executáveis na API
Idioma, tamanho de arquivo, superfície pública, escopo de tenant, injeção com forwardRef e decimal em texto.
5 verificações estáticas no app
Contrato da API, CSP do build, idioma, tokens de design e arquitetura, todas no CI.
CI nos dois repositórios
Lint sem warning, formatação, tipos, testes, e2e com Postgres, build, auditoria de dependências, Docker e varredura de segredos.
Hooks de pré-commit e pré-push
Rodam o mesmo lint do CI, em container, sobre o que está no index.
Testes conferidos por mutação
Quebrar o código, ver o teste falhar, restaurar. Um teste que passa de qualquer jeito não documenta nada.
11 ADRs e memória viva
Decisões registradas com o motivo, pegadinhas que já custaram tempo e um journal por sessão, em português e inglês.
Documentação de 18 pra 56 arquivos
36 na API e 20 no app, com README que descreve o que o código faz de verdade.
Confiabilidade
Como a operação passou a se comportar
Resultado
O que a virada entregou na prática
Sistema em produção no Cloud Run
Infraestrutura inteira em Terraform, no lugar da VM com deploy manual.
33 achados da auditoria tratados por fases
7 críticos, 9 altos, 12 médios e 5 baixos, cada um com evidência.
487 casos de teste na API
366 unitários e 121 e2e contra Postgres real, de 11.
482 casos de teste no app
De 3 casos num arquivo só.
Nenhum arquivo acima do teto
500 linhas na API, 400 por componente no app, impostos por guarda.
Sessão inteira em cookie httpOnly
Access de 15 minutos e refresh rotativo, fora do alcance do JavaScript.
Banco sob baseline e migrations
synchronize desligado em todo ambiente.
Angular 21 e design system com tokens
Zero cor crua nos estilos. Antes, 951.
11 guardas e verificações que reprovam o build
6 na API e 5 no app, cada uma nascida de um erro real.
56 documentos entre ADRs, memória e journal
De 18, nos dois repositórios.
Próximo projeto
Webinário sobre Monitoramento de Máscaras e Temperatura