Base/labs | Manual CRM
Manual 03 / 06

O banco do CRM

O CRM do grupo vive em um PostgreSQL no Supabase e substitui as planilhas. Você acessa direto pelo Claude Code ou Claude Cowork, conversando em português — o Claude traduz seu pedido em SQL e executa.

1Banco de dados

Oportunidades, contatos, empresas, leads, tarefas e propostas. As entidades principais e como se conectam:

PK chave primária FK chave estrangeira (aponta para outra tabela) LK tabela de lookup / configuração
companiesempresas
  • PKidinteger
  • nametext
  • FKindustry_id→ industries
  • domain_nametext
  • cnpjtext
  • emailtext
  • produtos_servicostext
  • FKcnae_section_code→ setor
  • FKcnae_division_code→ segmento
contactscontatos
  • PKidinteger
  • first_name / last_name
  • linkedin_url · site · cargotext
  • FKlead_status_id→ lookup
  • e-mails / telefones→ contact_channels
contact_channelsformas de contato
  • PKidinteger
  • FKcontact_id→ contacts
  • typeemail/phone…
  • valuetext
  • is_primary / is_activebool
dealsoportunidades / propostas
  • PKidinteger
  • name / value · numero_proposta
  • template · public_url · drive_folder_id
  • FKcompany_id→ companies
  • FKowner_id→ users
  • FKbrand_id→ brands
  • FKstage_id→ deal_stages
leadsleads
  • PKidinteger
  • name / email
  • FKstatus_id→ lead_statuses
taskstarefas
  • PKidinteger
  • title / due_date
  • FKowner_id→ users
cnae_sectionssetor · LK
  • PKcodetext
  • nametext
cnae_divisionssegmento · LK
  • PKcodetext
  • FKsection_code→ setor
  • nametext
brands · pipelines · …config · LK
  • deal_stages
  • industries
  • lead_statuses
  • marketing_statuses

O que você pode fazer

Buscar, inserir, alterar e deletar registros (deals — que são as oportunidades e propostas —, contatos, empresas, leads, tarefas, atividades). O que você não pode: mexer na estrutura do banco (criar/apagar tabelas ou colunas) — é proposital e garantido pelo próprio banco.

Formas de contato

Um contato pode ter vários e-mails e telefones — eles ficam na tabela contact_channels (uma linha por canal), não em colunas do contacts. Cada canal tem type (email, phone, mobile, whatsapp…), is_primary (o principal daquele tipo) e is_active. Para "o e-mail da Maria", peça o canal type='email' e is_primary; para desativar um número antigo sem perdê-lo, marque is_active=false.

Pipelines e estágios

Cada marca tem seu pipeline (Base/labs, Base2, Crowdtest) com estágios próprios — refletindo exatamente os estágios do HubSpot. Um deal não aponta direto para o pipeline: ele tem um stage_id, e o estágio é que pertence a um pipeline (deal → stage_id → deal_stages → pipeline). O brand_id do deal indica a marca. Para "deals da Base/labs por estágio", junte deals com deal_stages e filtre pelo pipeline.

Identificadores

Toda entidade tem um id próprio do nosso banco (a chave que você usa nas relações). O hubspot_id, quando existe, é só referência ao registro de origem no HubSpot — não use como chave nas suas consultas.

2Usar

Com a variável CRM_PG_URL configurada (veja Instalação), abra o Claude e fale naturalmente. Exemplos:

claude code
Liste os 10 deals mais recentes da Base/labs com valor acima de 50 mil.

Crie um contato: Maria Silva, maria@acme.com, empresa Acme.

Mude o estágio do deal 26-BL-040 para "Negociação".

Quantas tarefas em aberto o Tales tem?

O Claude monta o SQL e executa via psql com a sua credencial — a auditoria registra quem rodou.

3Classificação de empresas

Cada empresa pode ser classificada em dois níveis, seguindo o padrão CNAE (a taxonomia oficial do IBGE, que o mercado brasileiro usa):

  • Setor = Seção CNAE — 21 grandes áreas (ex.: J — Informação e comunicação, F — Construção). Coluna cnae_section_code, lookup em cnae_sections.
  • Segmento = Divisão CNAE — ~87 subdivisões (ex.: 62 — Serviços de TI, 41 — Construção de edifícios). Coluna cnae_division_code, lookup em cnae_divisions. Cada Segmento pertence a um Setor.

Exemplos de pedido ao Claude:

claude code
Quantas empresas temos por setor?

Liste as empresas do segmento "Serviços de TI" (divisão 62).

Mostre os deals abertos de empresas do setor de Construção.

Nem toda empresa está classificada

A classificação foi importada do HubSpot (campo "Segmento") e complementada automaticamente a partir do domínio do site. Empresas sem o dado ficam com cnae_section_code/cnae_division_code em branco — o enriquecimento é contínuo. A coluna antiga industry_id permanece como referência, mas a classificação oficial passou a ser o CNAE.

4Tabelas de configuração são só leitura

As tabelas que definem a configuração do CRM — brands, pipelines, deal_stages, industries, lead_statuses, marketing_statuses, e os lookups de classificação cnae_sections (Setor) e cnae_divisions (Segmento) — você consegue consultar, mas não alterar. Mudar um estágio de pipeline afeta todo mundo, então isso fica restrito aos administradores. Se precisar de um novo estágio ou marca, fale com o Hugo.

5Auditoria

Toda inserção, alteração e remoção fica registrada em uma tabela de auditoria, identificando o usuário, o que mudou e quando. Isso é automático — você não precisa fazer nada. Serve para reconstruir o histórico se algo for alterado por engano.

Errou em uma alteração?

Como tudo é auditado, dá para recuperar o valor anterior. Fale com o Hugo com o máximo de detalhe (qual registro, quando) que ele localiza no log de auditoria.

6Para administradores

restrito

O acesso via psql permite apenas dados — de propósito, ninguém altera a estrutura do banco por engano. Quem precisa mexer na estrutura (criar/alterar tabelas e colunas, rodar migrations) usa o MCP oficial do Supabase, autenticado com um Personal Access Token (PAT) pessoal.

Só para hbarros e robert

O PAT dá acesso administrativo total ao projeto Supabase — não há como restringi-lo a "só estrutura". O time comum não configura este MCP; usa apenas o psql via CRM_PG_URL.

No dia a dia o admin usa o psql (CRUD, auditado pela sua role); o MCP do Supabase fica reservado para alterações de estrutura.

terminal
# gere seu PAT em supabase.com/dashboard/account/tokens (trate como senha de admin)
claude mcp add supabase \
  --env SUPABASE_ACCESS_TOKEN="SEU_PAT" \
  -- npx -y @supabase/mcp-server-supabase@latest --project-ref=qeocffkayfgstlyoeltr

Boa prática

Mesmo sendo admin, prefira o psql via CRM_PG_URL para inserir/alterar dados — assim a auditoria registra você, não o admin genérico. Use o MCP do Supabase só quando o pedido for sobre estrutura.

7Row Level Security está desligado

restrito

Todas as tabelas do schema public estão hoje com RLS desligado (decisão de junho de 2026). O motivo: enquanto o banco for acessado por conexões administrativas de confiança — não um app exposto à internet — o RLS sem políticas só atrapalha. Com RLS ligado e nenhuma política definida, o Postgres aplica deny-all e nenhuma linha é visível.

Gatilho para reativar

No dia em que o banco for exposto a um app cliente / frontend (que usa a anon key pública do Supabase), o RLS precisa ser reativado e acompanhado de políticas. Sem isso, qualquer pessoa com a URL e a chave pública lê e escreve as tabelas inteiras. O RLS é a única camada de segurança entre o cliente e os dados.

sql
ALTER TABLE public.companies ENABLE ROW LEVEL SECURITY;
-- depois: CREATE POLICY ... ON public.companies FOR SELECT USING (...);

8Email marketing

O CRM integra com o SendGrid para disparar campanhas de email e medir resultados. A lógica é: o CRM é dono da lista; o SendGrid dispara; os resultados voltam para o CRM. Uma campanha nasce de um segmento de contatos do banco, vira uma lista no SendGrid, e o marketing finaliza o email e envia pelo painel.

Três tabelas guardam isso: campaigns (uma por campanha/marca), campaign_recipients (quem estava na lista no disparo) e campaign_events (resultados trazidos do SendGrid). Você consulta resultados em linguagem natural, como o resto do CRM.

claude code
Qual a taxa de abertura da última campanha da Base/labs?

Quantos cliques e descadastros teve a campanha #12?

Liste os contatos que descadastraram no último mês.

Como uma campanha é montada

  1. Sincronizar — o script email-marketing/bin/sync-campaign.mjs resolve o segmento de contatos, cria a lista no SendGrid, grava o snapshot de destinatários no CRM e cria o email como rascunho. Por padrão o segmento são todos os contatos com email ativo marcados como aptos a marketing.
  2. Disparar — o marketing entra no painel do SendGrid, finaliza o conteúdo do email e clica enviar. O disparo é manual, de propósito.
  3. Buscar resultados — um job rodado pelo nosso lado consulta a API do SendGrid de tempos em tempos: pega os números agregados da campanha e a lista de quem descadastrou/bounceou, e grava no CRM. Quem descadastra é marcado para não receber campanhas futuras.

Descadastro é respeitado automaticamente

Quem descadastra ou tem bounce permanente entra na lista de supressão do SendGrid. O job de busca traz essa lista e atualiza o marketing_status do contato no CRM para Descadastrado (ou Inválido no caso de bounce). Na próxima campanha ele já sai do segmento — você não precisa fazer nada.

Configuração no SendGrid restrito · faz uma vez

Antes da primeira campanha, um administrador precisa configurar o SendGrid. É manual (fora do código) e se faz uma vez por marca:

  • Autenticação de domínio por marca — em Settings → Sender Authentication, autenticar o domínio de cada marca (Base2, Base/labs, MeloQA, Crowdtest) com os registros SPF/DKIM. Isso separa a reputação de envio de cada marca e melhora a entregabilidade. Cada marca tem seu próprio sender (remetente).
  • Grupo de descadastro (Unsubscribe Group) por marca — garante o rodapé de "descadastrar" no email e a página de preferências, exigência de LGPD. Toda campanha usa o grupo da sua marca.
  • Chave de API com escopo Marketing + Stats — confirmar que a SENDGRID_API_KEY em uso tem permissão de Marketing (Contacts, Lists, Single Sends) e de leitura de Stats/Suppressions, para o job conseguir buscar os resultados.

Como buscamos os resultados modelo atual

Os resultados são trazidos por busca ativa (pull): um job nosso, rodado localmente, consulta a API do SendGrid de tempos em tempos e grava no CRM. Não há nada do SendGrid "ligando para a gente" — somos nós que perguntamos. Isso evita precisar de um servidor público no ar 24/7 e mantém tudo do nosso lado. O job busca duas coisas:

  • Números agregados da campanha — via Single Send Stats API: enviados, entregues, aberturas (e aberturas únicas), cliques (e únicos), bounces, spam, descadastros. Grava o resumo na campanha.
  • Listas de supressão — via Suppressions / Bounces API: quem descadastrou, bounceou ou marcou spam, com data. Cruza com os contatos e atualiza o marketing_status no CRM.

Limitação do modelo pull: sem "quem abriu/clicou"

A API de marketing do SendGrid entrega os totais da campanha (ex.: "120 aberturas, 30 cliques") e as listas de descadastro/bounce, mas não diz qual contato específico abriu ou clicou. Esse detalhe por-pessoa só existe pelo caminho de webhook (push), descrito abaixo. Para acompanhar desempenho de campanha e respeitar descadastros, o pull é suficiente; para "o contato X abriu", não.

O job é o script email-marketing/bin/pull-campaign-stats.mjs. Os números agregados ficam na tabela campaign_statsuma linha por campanha e por dia, então dá para ver a evolução da campanha ao longo do tempo. As atualizações de descadastro/bounce vão direto no marketing_status do contato.

terminal
cd swl-crm/email-marketing   # pasta da integração (npm install na 1ª vez)

# busca stats de todas as campanhas ativas + sincroniza descadastros
node bin/pull-campaign-stats.mjs all

# só uma campanha
node bin/pull-campaign-stats.mjs stats --campaign=12

Como o job roda do nosso lado, ele pode rodar numa máquina local — não precisa de endereço público. Para gravar no CRM usa psql com a sua credencial (variável CRM_PG_URL), então a auditoria registra quem rodou — sem chave de admin. Precisa também da SENDGRID_API_KEY (com leitura de Stats/Suppressions). O agendamento fica a cargo de um cron do nosso lado, no intervalo que fizer sentido (ex.: de hora em hora enquanto a campanha está ativa).

Webhook em tempo real ligado · captura por contato

Para captura por contato (saber exatamente quem abriu/clicou) e em tempo real, usamos o Event Webhook: o SendGrid faz uma chamada HTTP para um endereço nosso a cada evento. Como isso exige um endereço público no ar 24/7, roda como Cloudflare Worker (conta SWL, ex-Redes@base2) — não numa máquina local atrás de firewall, onde o SendGrid não alcançaria. Ligado em junho de 2026.

  • Worker: email-marketing/sendgrid-events, publicado em https://sendgrid-events.baselabs.com.br (custom domain na zona baselabs, SSL automático). Valida a assinatura ECDSA do "Signed Event Webhook", grava cada evento em campaign_events (dedup por sg_event_id) e atualiza marketing_status em descadastro/bounce.
  • Como foi ligado: wrangler deploy + segredos SENDGRID_WEBHOOK_PUBLIC_KEY / SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY (via wrangler secret put); no painel, Settings → Event Webhook apontando para a URL acima com "Signed Event Webhook" ativado.
  • Atribuição de campanha: Single Send não aceita custom_args, então o sync-campaign.mjs carimba uma categoria campaign:<id> em cada envio; o webhook devolve essa categoria em todo evento e o worker reconstrói o campaign_id. O contato é resolvido pelo email.

O webhook complementa o pull: o pull (acima) traz os agregados históricos; o webhook traz o evento por-contato em tempo real. Eventos de clique só aparecem com o click tracking ligado — hoje ele está desligado para preservar o link de descadastro (ver aviso abaixo).

Disparo é sempre pelo painel importante

Não dá para disparar campanha por API — só pelo painel

Testado em junho de 2026: disparar um Single Send via API (PUT /v3/marketing/singlesends/{id}/schedule com send_at:"now") é aceito mas não envia nesta conta — a campanha fica registrada com 0 envios e ninguém recebe, sem erro visível. O disparo só funciona clicando "Send" no painel do SendGrid (Marketing → Single Sends). Por isso o fluxo é: o sync-campaign.mjs prepara o rascunho, e o envio é sempre manual no painel. Não automatize o disparo.

Link de descadastro e click tracking

O link de "Descadastrar" passava pelo domínio de rastreamento de cliques do SendGrid (url8026.baselabs.com.br), que não tem certificado SSL válido sem abrir um ticket no suporte — daí o erro de "conexão não é particular" ao clicar. Solução provisória: click tracking desligado, então os links abrem direto (e o descadastro funciona), ao custo de não capturar o evento de clique. Solução definitiva: abrir o ticket de SSL do Link Branding e religar o click tracking.

9Playbook comercial

Guias de vendas das duas marcas do grupo — ICP, soluções, metodologia comercial, objeções, KPIs e valores. Consulte antes de qualificar um lead ou montar uma proposta.

→ Abrir Playbooks (Base/Labs e Base2)