Pular para o conteúdo
Voltar aos projetos

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

GCPWhatsApp API (Meta)IA/LLMCI/CD
Plannus CRM · frente de trabalho
  • 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

Deploy manual com PM2 e nginx numa única VM
Cloud Run atrás de um balanceador global, tudo em Terraform
Schema alterado pelo ORM; 53 migrations que não criavam o banco do zero
synchronize desligado em todo ambiente; baseline e migrations que sobem um banco vazio
Sessão de 7 dias num token só, sem refresh nem revogação
Access token de 15 min e refresh rotativo, os dois em cookie httpOnly
Trabalho em segundo plano em memória, perdido a cada reinício
Outbox transacional: o trabalho é gravado antes da resposta e sobrevive a reinício
CORS aberto, sem cabeçalhos de segurança nem limite de requisições
CORS por allowlist, helmet e rate limit por rota
4 arquivos de teste pra 36 mil linhas de API
72 arquivos e 487 casos de teste, com e2e contra Postgres real
Nenhum CI, nenhum hook, nenhuma regra automatizada
CI com lint, tipos, testes, e2e, build, auditoria de dependências e 6 guardas
Angular 18 e 951 cores escritas à mão nos estilos
Angular 21 e design system com tokens; cor crua reprova o build

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

Antes4
Depois72

De 11 casos de teste pra 487, 121 deles e2e contra Postgres real.

Casos de teste no app

casos de teste

Antes3
Depois482

De um arquivo gerado pelo CLI pra 84 arquivos de spec.

Maior arquivo da API

linhas

Antes2.698
Depois500

Teto de 500 linhas imposto por guarda. Nenhum arquivo acima dele, e o mapa de exceções está vazio.

Cobertura de linhas da API

48,7%

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

Críticos7
Altos9
Médios12
Baixos5

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

Token roubado
AntesVálido por 7 dias
Depois15 min, revogável
Trabalho em segundo plano no deploy
AntesPerdido ou duplicado
DepoisGravado antes da resposta
Ambiente novo a partir do repositório
AntesImpossível
DepoisMigrations do zero
Deploy
AntesÀ mão, numa VM
DepoisTerraform e Cloud Run
Regra do projeto quebrada
AntesPassava no build
DepoisReprova o build
Erro de banco pro cliente
AntesMensagem do driver
DepoisCódigo de erro estável

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

Ver estudo de caso