Docs LP Captacao AulaoGitHub ↗

Arquitetura

Visão técnica completa do sistema. Para operação no dia-a-dia veja docs/operacao/.

Stack

CamadaTecnologia
FrameworkNext.js 16 (App Router)
LinguagemTypeScript 5
EstilosTailwind CSS v4 (sem config file, usa @import "tailwindcss" + @theme)
BancoSupabase (Postgres gerenciado, região São Paulo)
ValidaçãoZod 4
AnimaçãoFramer Motion 12
Íconeslucide-react
TipografiaInter (body) + Sora (display/headlines)
HostingVercel
Domínioaulao.brunolucarelli.com.br (DNS gerenciado no Cloudflare)

Fluxo end-to-end

flowchart TD
  start([Lead acessa URL ativa]) --> pixel[Pixel inicializa<br/>2 pixels + PageView]
  pixel --> hero[Renderiza Hero<br/>com variante lp1/lp2/lp3]
  hero --> click1[Click PARTICIPAR]
  click1 --> modal[Modal abre<br/>nome + email + telefone + LGPD]
  modal --> submit1[Submit Etapa 1]

  submit1 --> api1["POST /api/captacao"]
  api1 --> dbcapt[(INSERT captacoes)]
  dbcapt --> trigger1[(Trigger cria/resolve lead_id<br/>em public.leads)]
  trigger1 --> resp1["Response {leadId, captacaoId}"]

  resp1 -.background after.-> ac[ActiveCampaign<br/>sync + tag + nota]
  resp1 -.background after.-> capi1[Meta CAPI Lead<br/>Pixel PRINCIPAL]
  resp1 -.background after.-> uni[Unnichat<br/>fluxo API + WhatsApp]
  resp1 -.background after.-> mk1[Make Etapa 1<br/>backup em planilha]

  resp1 --> pix1[Client: fbq Lead<br/>Pixel PRINCIPAL]
  pix1 --> sess1[sessionStorage.aulao_lead]
  sess1 --> goto2["Redirect /cadastro2"]

  goto2 --> etapa2[7 perguntas com auto-scroll]
  etapa2 --> submit2[Submit Etapa 2]

  submit2 --> api2["POST /api/pesquisa"]
  api2 --> score[Calcula score 3 eixos<br/>+ classifica 1 dos 12 perfis]
  score --> qualif{Qualificado?<br/>idade 25-55 AND<br/>renda>=6k OR cap>=30k}
  qualif --> dbpesq[(INSERT pesquisas)]
  dbpesq --> resp2["Response {qualificado, perfil}"]

  resp2 -.background after.-> mk2[Make Etapa 2<br/>planilha + automacoes]
  qualif -- SIM --> capi2[CAPI CompleteRegistration<br/>NOS 2 PIXELS]
  qualif -- NAO --> skip[Pixel novo fica limpo<br/>sem evento]

  resp2 --> sess2[sessionStorage.aulao_pesquisa<br/>qualificado + eventId]
  sess2 --> goto3["Redirect /concluir-cadastro"]

  goto3 --> render3[Estatico: le linkGrupoWhatsapp<br/>de runtime.ts]
  render3 --> obrigado[Countdown 12s +<br/>botao Entrar no Grupo]
  obrigado --> qualifpx{Qualificado<br/>no sessionStorage?}
  qualifpx -- SIM --> pix2[Client: fbq CompleteRegistration<br/>nos 2 PIXELS]
  qualifpx -- NAO --> clear[Limpa sessionStorage]
  pix2 --> clear
  clear --> count[Countdown decrementa<br/>12 -> 0]
  count --> grupo([Redirect para grupo WhatsApp])

Estrutura de configs

┌─────────────────────────────────────────────────────────────────┐
│  Supabase: public.landing_configs                               │
│  Uma linha por código de aulão. Cada linha tem:                 │
│    • codigo, estado (ativa | standby | arquivada)               │
│    • lancamento_id, etapa_lancamento_id                         │
│    • tag_activecampaign                                         │
│    • link_grupo_whatsapp                                        │
│    • url_webhook_unnichat + unnichat_series_id                  │
│    • url_webhook_make_etapa1                                    │
│    • url_webhook_make_etapa2                                    │
└─────────────────────────────────────────────────────────────────┘
                          ↓ (lido pelo endpoint /api/admin/virar)
┌─────────────────────────────────────────────────────────────────┐
│  src/config/runtime.ts                                          │
│  Gerado AUTOMATICAMENTE via GitHub Contents API a cada virada   │
│  ou sync. Carrega TODAS as 5 configs operacionais. Versionado   │
│  no Git (cada virada vira commit).                              │
└─────────────────────────────────────────────────────────────────┘
                          ↓ (importado pelas rotas no build)
┌─────────────────────────────────────────────────────────────────┐
│  Rotas no Vercel Edge (estáticas - zero query em runtime)       │
│  /cadastro, /cadastro-fb-lp{1,2,3}, /cadastro-yt-lp{1,2,3}      │
│  /cadastro2, /concluir-cadastro                                 │
└─────────────────────────────────────────────────────────────────┘

E o conteúdo da landing (copy, imagens, variantes) vive separado:

src/config/
├── launches/
│   ├── 2605_A2.ts       (Maio - conteúdo + 1 variante)
│   ├── 2606_A1.ts       (Junho - conteúdo + 3 variantes lp1/lp2/lp3)
│   ├── index.ts         (lista das configs disponíveis)
│   └── types.ts         (tipos compartilhados)
├── active.ts            (re-exporta o aulão ATIVO)
└── runtime.ts           (configs operacionais - gerado)

Tabelas do banco central Arrematador

Não usamos um banco isolado. Tudo escrito no banco central (public.captacoes, public.leads, public.pesquisas etc).

public.landing_configs (nossa tabela de controle)

ColunaTipoFunção
iduuidPK
codigotext uniqueIdentificador do aulão (2606_A1)
estadotextativa | standby | arquivada (constraint check)
lancamento_idint FKReferência para public.lancamentos
etapa_lancamento_idint FKReferência para public.etapas_lancamento
tag_activecampaigntextTag aplicada nos leads no AC
link_grupo_whatsapptextURL do grupo na /concluir-cadastro
url_webhook_unnichattextWebhook Unnichat (Etapa 1)
unnichat_series_idtextSeries ID do Unnichat
url_webhook_make_etapa1textWebhook Make (Etapa 1)
url_webhook_make_etapa2textWebhook Make (Etapa 2)
descricaotextNota livre
ativada_em, arquivada_em, criada_emtimestamptzAuditoria

Constraint importante: índice unique em (estado) filtrado por where estado = 'ativa' → garante que só pode existir 1 linha ativa por vez (race conditions impossíveis).

Tabelas do banco central que escrevemos

TabelaQuandoO que
public.captacoesEtapa 1 (sincrono)INSERT com email + nome + telefone + UTMs + lançamento/etapa + pagina_nome
public.leadsEtapa 1 e 2 (trigger)Lead consolidado, resolve por email_oficial ou telefone_sufixo
public.pesquisasEtapa 2 (sincrono)INSERT com 7 respostas + perfil calculado + cep

Integrações externas

SistemaQuando disparaO que envia
ActiveCampaignEtapa 1 (background)Cria/atualiza contato + aplica tag + nota com UTMs
Meta Pixel clientEtapa 1 (browser)fbq Lead no Pixel PRINCIPAL
Meta CAPI serverEtapa 1 (background)Lead server-to-server no Pixel PRINCIPAL (dedup com client via eventId)
Unnichat webhookEtapa 1 (background)Payload com series + contact → dispara fluxo API + WhatsApp
Make.com Etapa 1Etapa 1 (background)Payload completo (18 campos) → backup em planilha
Meta Pixel clientEtapa 2 obrigado (browser)fbq CompleteRegistration nos 2 pixels (só qualificados)
Meta CAPI serverEtapa 2 (background)CompleteRegistration server-to-server (só qualificados)
Make.com Etapa 2Etapa 2 (background)Payload com 13 campos incluindo perfil + qualificado

Score e classificação (Etapa 2)

3 eixos pontuam o lead:

EixoScore 0-NOrigem
Renda0-6Resposta "Renda mensal da família"
Capital0-7Resposta "Quanto tem disponível"
Experiência0-2Resposta "Já arrematou imóvel?"

Renda alta = renda_score ≥ 4 (acima de R$ 8.000) Capital alto = capital_score ≥ 4 (acima de R$ 50 mil)

12 perfis = combinação 4 grupos × 3 níveis de experiência:

GrupoRenda + CapitalExp 0Exp 1Exp 2
PoupadorBaixa + BaixoEm FormaçãoEm EvoluçãoExperiente
FinanciadorAlta + BaixoInicianteEm EvoluçãoEstratégico
CapitalizadoBaixa + AltoInicianteEm EvoluçãoEstratégico
PotencialAlta + AltoEstreante c/ Alto PotencialEm AceleraçãoPotencial Exponencial

Regra de qualificação Meta (decide se dispara CompleteRegistration):

idade entre 25-59 anos
  AND
(renda_score >= 3  OR  capital_score >= 3)

(renda_score = 3 é a faixa R$ 6.000,01 a R$ 8.000,00; capital_score = 3 é "Entre R$ 31 a R$ 50 mil")

Sistema de virada automatizada

Dois mecanismos paralelos:

Vercel Cron (agendado)

vercel.json declara crons com path + schedule. Vercel chama o endpoint no horário marcado. Auth via header Authorization: Bearer {CRON_SECRET}.

Endpoint manual

/api/admin/virar/[codigo]?token=arrematador-01 aceita query string com o ADMIN_PREVIEW_TOKEN.

Os 2 modos chamam o mesmo endpoint, que faz:

  1. SELECT na linha do codigo em landing_configs
  2. Se já é ativa: pula 3 e 4, vai direto pra 5
  3. UPDATE no banco: arquiva antiga, ativa nova
  4. Edita src/config/active.ts via GitHub Contents API
  5. Edita src/config/runtime.ts via GitHub Contents API
  6. Commits disparam deploy Vercel (~30-60s)

Sistema de variantes visuais

Cada aulão tem 1 ou mais "variantes" — mesma config, mesma copy, mas backgrounds e posicionamento diferentes:

variantes: {
  lp1: { bgDesktop, bgMobile, heroPosition: 'right' },
  lp2: { bgDesktop, bgMobile, heroPosition: 'left' },
  lp3: { bgDesktop, bgMobile, heroPosition: 'center-bottom' },
}

Cada wrapper de rota passa variante="lpN" para o componente, que lê o objeto correspondente. Se a variante não existe na config ativa, a rota redireciona para /cadastro (não dá 404 — preserva tráfego pago).

Performance

  • Páginas estáticas: todas as URLs públicas são pré-renderizadas no build do Vercel. Resposta em ~50ms do CDN
  • Zero query em runtime: configs operacionais vêm do runtime.ts estático. Banco só é tocado nos POST das APIs e no /api/admin/virar
  • Background tasks: integrações externas (AC, CAPI, Make, Unnichat) rodam em Vercel after() — não bloqueiam o response pro lead
  • Cache do Pixel: o script da Meta é carregado via next/script com strategy="afterInteractive" — não atrasa o LCP

Stack de roteamento

RotaRenderFunção
/redirect 308Para /cadastro
/cadastroEstáticaLP orgânica (lp1)
/cadastro-fb-lp1, /cadastro-yt-lp1EstáticasTráfego pago LP1
/cadastro-fb-lp2, /cadastro-yt-lp2EstáticasTráfego pago LP2
/cadastro-fb-lp3, /cadastro-yt-lp3EstáticasTráfego pago LP3
/cadastro2EstáticaEtapa 2 (7 perguntas)
/concluir-cadastroEstáticaEtapa 3 (countdown + grupo)
/preview/[codigo]DinâmicaPreview admin (cookie required)
/admin/preview-on, /admin/preview-offDinâmicaToggle do cookie
/api/captacaoDinâmicaPOST etapa 1
/api/pesquisaDinâmicaPOST etapa 2
/api/admin/virar/[codigo]DinâmicaVirada de aulão

Variáveis de ambiente

VariávelOndeFunção
NEXT_PUBLIC_SUPABASE_URLVercel + .envURL do projeto Supabase
NEXT_PUBLIC_SUPABASE_ANON_KEYVercel + .envAnon key (safe to expose)
SUPABASE_SERVICE_ROLE_KEYVercel + .envService role (server only)
ACTIVECAMPAIGN_API_URLVercel + .envURL da conta AC
ACTIVECAMPAIGN_API_KEYVercel + .envAPI key
META_CAPI_ACCESS_TOKENVercel + .envToken CAPI (vale pros 2 pixels)
ADMIN_PREVIEW_TOKENVercel + .envToken para ativar cookie de preview admin
CRON_SECRETVercel + .envSecret que Vercel Cron envia no header
GITHUB_PATVercel + .envPAT para o endpoint editar active.ts e runtime.ts
GITHUB_OWNERVercel + .envfelipe-lucarelli
GITHUB_REPOVercel + .envlp-captacao-aulao