Arquitetura
Visão técnica completa do sistema. Para operação no dia-a-dia veja
docs/operacao/.
Stack
| Camada | Tecnologia |
|---|---|
| Framework | Next.js 16 (App Router) |
| Linguagem | TypeScript 5 |
| Estilos | Tailwind CSS v4 (sem config file, usa @import "tailwindcss" + @theme) |
| Banco | Supabase (Postgres gerenciado, região São Paulo) |
| Validação | Zod 4 |
| Animação | Framer Motion 12 |
| Ícones | lucide-react |
| Tipografia | Inter (body) + Sora (display/headlines) |
| Hosting | Vercel |
| Domínio | aulao.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)
| Coluna | Tipo | Função |
|---|---|---|
id | uuid | PK |
codigo | text unique | Identificador do aulão (2606_A1) |
estado | text | ativa | standby | arquivada (constraint check) |
lancamento_id | int FK | Referência para public.lancamentos |
etapa_lancamento_id | int FK | Referência para public.etapas_lancamento |
tag_activecampaign | text | Tag aplicada nos leads no AC |
link_grupo_whatsapp | text | URL do grupo na /concluir-cadastro |
url_webhook_unnichat | text | Webhook Unnichat (Etapa 1) |
unnichat_series_id | text | Series ID do Unnichat |
url_webhook_make_etapa1 | text | Webhook Make (Etapa 1) |
url_webhook_make_etapa2 | text | Webhook Make (Etapa 2) |
descricao | text | Nota livre |
ativada_em, arquivada_em, criada_em | timestamptz | Auditoria |
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
| Tabela | Quando | O que |
|---|---|---|
public.captacoes | Etapa 1 (sincrono) | INSERT com email + nome + telefone + UTMs + lançamento/etapa + pagina_nome |
public.leads | Etapa 1 e 2 (trigger) | Lead consolidado, resolve por email_oficial ou telefone_sufixo |
public.pesquisas | Etapa 2 (sincrono) | INSERT com 7 respostas + perfil calculado + cep |
Integrações externas
| Sistema | Quando dispara | O que envia |
|---|---|---|
| ActiveCampaign | Etapa 1 (background) | Cria/atualiza contato + aplica tag + nota com UTMs |
| Meta Pixel client | Etapa 1 (browser) | fbq Lead no Pixel PRINCIPAL |
| Meta CAPI server | Etapa 1 (background) | Lead server-to-server no Pixel PRINCIPAL (dedup com client via eventId) |
| Unnichat webhook | Etapa 1 (background) | Payload com series + contact → dispara fluxo API + WhatsApp |
| Make.com Etapa 1 | Etapa 1 (background) | Payload completo (18 campos) → backup em planilha |
| Meta Pixel client | Etapa 2 obrigado (browser) | fbq CompleteRegistration nos 2 pixels (só qualificados) |
| Meta CAPI server | Etapa 2 (background) | CompleteRegistration server-to-server (só qualificados) |
| Make.com Etapa 2 | Etapa 2 (background) | Payload com 13 campos incluindo perfil + qualificado |
Score e classificação (Etapa 2)
3 eixos pontuam o lead:
| Eixo | Score 0-N | Origem |
|---|---|---|
| Renda | 0-6 | Resposta "Renda mensal da família" |
| Capital | 0-7 | Resposta "Quanto tem disponível" |
| Experiência | 0-2 | Resposta "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:
| Grupo | Renda + Capital | Exp 0 | Exp 1 | Exp 2 |
|---|---|---|---|---|
| Poupador | Baixa + Baixo | Em Formação | Em Evolução | Experiente |
| Financiador | Alta + Baixo | Iniciante | Em Evolução | Estratégico |
| Capitalizado | Baixa + Alto | Iniciante | Em Evolução | Estratégico |
| Potencial | Alta + Alto | Estreante c/ Alto Potencial | Em Aceleração | Potencial 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:
- SELECT na linha do
codigoemlanding_configs - Se já é
ativa: pula 3 e 4, vai direto pra 5 - UPDATE no banco: arquiva antiga, ativa nova
- Edita
src/config/active.tsvia GitHub Contents API - Edita
src/config/runtime.tsvia GitHub Contents API - 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.tsestá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/scriptcomstrategy="afterInteractive"— não atrasa o LCP
Stack de roteamento
| Rota | Render | Função |
|---|---|---|
/ | redirect 308 | Para /cadastro |
/cadastro | Estática | LP orgânica (lp1) |
/cadastro-fb-lp1, /cadastro-yt-lp1 | Estáticas | Tráfego pago LP1 |
/cadastro-fb-lp2, /cadastro-yt-lp2 | Estáticas | Tráfego pago LP2 |
/cadastro-fb-lp3, /cadastro-yt-lp3 | Estáticas | Tráfego pago LP3 |
/cadastro2 | Estática | Etapa 2 (7 perguntas) |
/concluir-cadastro | Estática | Etapa 3 (countdown + grupo) |
/preview/[codigo] | Dinâmica | Preview admin (cookie required) |
/admin/preview-on, /admin/preview-off | Dinâmica | Toggle do cookie |
/api/captacao | Dinâmica | POST etapa 1 |
/api/pesquisa | Dinâmica | POST etapa 2 |
/api/admin/virar/[codigo] | Dinâmica | Virada de aulão |
Variáveis de ambiente
| Variável | Onde | Função |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Vercel + .env | URL do projeto Supabase |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Vercel + .env | Anon key (safe to expose) |
SUPABASE_SERVICE_ROLE_KEY | Vercel + .env | Service role (server only) |
ACTIVECAMPAIGN_API_URL | Vercel + .env | URL da conta AC |
ACTIVECAMPAIGN_API_KEY | Vercel + .env | API key |
META_CAPI_ACCESS_TOKEN | Vercel + .env | Token CAPI (vale pros 2 pixels) |
ADMIN_PREVIEW_TOKEN | Vercel + .env | Token para ativar cookie de preview admin |
CRON_SECRET | Vercel + .env | Secret que Vercel Cron envia no header |
GITHUB_PAT | Vercel + .env | PAT para o endpoint editar active.ts e runtime.ts |
GITHUB_OWNER | Vercel + .env | felipe-lucarelli |
GITHUB_REPO | Vercel + .env | lp-captacao-aulao |