Ir para o conteúdo

Infraestrutura — Brandvelia

Documento operacional dos serviços conectados a este monorepo. Atualizar quando a infra mudar.

Serviços

Serviço Conta/Projeto Ref
GitHub EuVinicios/mypaidtraffic (privado) —
Vercel team vinicios-silveiras-projects, projeto mypaidtraffic prj_Ho3yFRvdhSqOA046uBeeRgOws4zp
Supabase projeto panoptes (compartilhado, São Paulo) koknxysqvdvxrtgyjbsv
Cloudflare zona brandvelia.com (Registrar + DNS) zona f5424150e857d92628718facac2fdf42

Domínios

Domínio Uso Configuração
brandvelia.com Site público, landing/bridge pages DNS: A @ → 76.76.21.21 (DNS only)
app.brandvelia.com Central administrativa (fase 1) DNS: CNAME app → cname.vercel-dns.com (DNS only)
docs.brandvelia.com Documentação técnica MkDocs Vercel (brandvelia-docs, deploy via pnpm docs:deploy)
www.brandvelia.com Redirect 308 → apex DNS: CNAME www → cname.vercel-dns.com (DNS only)
  • Deploy: push para main (via PR) → produção em brandvelia.com e app.brandvelia.com.
  • Protection: URLs *.vercel.app exigem SSO; domínios custom são públicos (all_except_custom_domains).
  • Root directory do projeto Vercel: apps/web.

Supabase (regras de banco compartilhado)

  • O projeto panoptes atende outros sistemas em produção (Finanças, Conexao, Radar, Viagens, Z900, Panoptes/ações). Nunca alterar/dropar objetos fora do schema brandvelia.
  • Migrations: additive-only, aplicadas com supabase db push (a partir da raiz). Aplique antes do deploy do código que depende delas (o app chama RPCs e colunas novas).
  • Antes de migrations estruturais: supabase db dump --linked -f backup.sql.
  • Placeholders _shared_panoptes.sql em supabase/migrations/ representam o histórico de migrations de outros sistemas — não editar, não remover.

Padrão de acesso a dados (importante)

O projeto não altera a configuração de schemas expostos da API do Supabase (configuração compartilhada). Em vez disso:

  • Tabelas vivem no schema brandvelia com RLS habilitado (isolamento por workspace_id via workspace_members).
  • A API REST as acessa por views auto-atualizáveis public.bv_* com security_invoker = true — o RLS das tabelas base se aplica normalmente.
  • Funções SQL têm wrappers públicos: bv_create_workspace, bv_get_dashboard_summary.
  • anon só enxerga landing pages published (via policy específica). authenticated só vê dados dos workspaces em que é membro. service_role é usado apenas nos endpoints públicos server-side (/api/track, /api/leads).
  • Métricas financeiras são calculadas em SQL (bv_get_dashboard_summary) — nunca por LLM.
  • Ao adicionar colunas em tabelas existentes, recriar a view bv_* correspondente (views congelam a lista de colunas).

IA (OpenRouter)

  • packages/ai — gateway server-only (OpenRouterProvider), structured outputs com JSON Schema.
  • Model registry: tabela bv_ai_models (seed com 4 modelos; preços aproximados — revisar em openrouter.ai/models).
  • Uso por chamada registrado em bv_ai_runs (tokens, custo, latência) + agregado em bv_ai_usage.
  • Env var obrigatória para IA: OPENROUTER_API_KEY (server-side, Vercel).

Secrets

Segredo Onde vive Env var
Supabase anon key Vercel (encrypted, 3 ambientes) + .env.local (dev) NEXT_PUBLIC_SUPABASE_ANON_KEY
Supabase service role Vercel (encrypted, 3 ambientes) + .env.local (dev) SUPABASE_SERVICE_ROLE_KEY
Cloudflare DNS API token ~/.cloudflare/brandvelia-dns-token (local, 600) — (infra)
OpenRouter API key Vercel (sensitive/encrypted, 3 ambientes) + .env.local (dev) OPENROUTER_API_KEY
ClickBank INS secret ~/.brandvelia/clickbank-ins-secret (local, 600) + Vercel (3 ambientes) — mesmo valor configurado no painel do ClickBank CLICKBANK_NOTIFICATION_SECRET
Turnstile keys pendente (opcional; formulários públicos) NEXT_PUBLIC_TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY
ClickBank API keys (Key 1 + Key 2) ~/.brandvelia/clickbank-api.json (local, 600) + Vercel (3 ambientes) CLICKBANK_API_KEY_1 / CLICKBANK_API_KEY_2
Token do agente de captura gerado em Conexões; só no servidor da captura (~/.brandvelia/agent.env, 600); banco guarda o hash BRANDVELIA_AGENT_TOKEN (no servidor, não na Vercel)
Telegram bot Vercel (3 ambientes) TELEGRAM_BOT_TOKEN (+ TELEGRAM_BOT_USERNAME, não secreto)

URLs públicas (não são segredos, mas precisam estar na Vercel): NEXT_PUBLIC_SITE_URL (https://brandvelia.com — pressels, INS) e NEXT_PUBLIC_APP_URL (https://app.brandvelia.com — destino do agente de captura).

IA (validação E2E)

Pipeline do Copilot testado ponta a ponta (jun/2026): sessão → workspace → transações → chamada OpenRouter (google/gemini-2.5-flash) → resposta citando os valores exatos calculados em SQL → registro em bv_ai_runs (tokens, custo, latência). Custo por chamada calculado a partir do model registry.

ClickBank INS (fase 2)

  • Webhook de produção: https://brandvelia.com/api/webhooks/clickbank
  • Formato: envelope JSON {notification, iv} (AES-256-CBC, key = sha1(secret)[:32]); legado form-encoded também aceito
  • Secret key: máx. 16 caracteres (limite da UI do ClickBank) — valor em ~/.brandvelia/clickbank-ins-secret
  • Pipeline: raw event (dedup_key único, replay-safe) → normalização (SALE/BILL/RFND/CGBK/INSF/CANCEL-REBILL/TEST_*/unmapped) → conversão → atribuição (INS 8.0: affiliateTrackingParameters.extclid + campaign nomeado; legado: trackingCodes) → transação (external_ref = receipt) → lead_events
  • TEST (botão Test URL) cria conversão com notes=test_notification, sem transação
  • Configuração no ClickBank (docs oficiais abril/2025 — INS é UI-only):
  • Login → selecionar o account nickname na aba Accounts
  • Aba Vendor Settings → My Site → seção Advanced Tools → Edit
  • Clique em "Request Access" ao lado do campo Instant Notification URL (o recurso não é habilitado por padrão) → preencha o formulário e concorde com os termos → Save Changes & Request API Access
  • Depois do acesso concedido: informe a Secret Key (≤16 chars) e salve
  • Entre a URL acima no campo Instant Notification URL → clique em Test URL (envia uma notificação TEST; a URL fica marcada como Verified) → Save Changes
  • Reconciliação: Finance → Importar CSV (relatório ClickBank) → matched/missing_in_app/missing_in_source

ClickBank API (fase 2)

Specs oficiais salvas em docs/clickbank-api/ (baixadas de docs.clickbank.net/docs/docs-manifest.json):

  • analytics-v1.json — GET /rest/1.3/analytics/{role}/{dimension} (role: vendor|affiliate; dimension: tracking_id, vendor, product_sku, customer_country, …); param obrigatório account (nickname)
  • orders-v2.json — GET /v2/orders (lista pedidos por período/tipo: SALE, BILL, RFND, CGBK, TEST_*)
  • products-v2.json — catálogo de produtos por seller

Autenticação: chave única com a role API Order Read em CLICKBANK_API_KEY_1 (header Authorization: <api-key>). O formato antigo <dev-key>:<clerk-key> da spec retorna 401 e não é usado.

Descobertas de sondagem (ago/2026):

  • developer.clickbank.com bloqueado por Cloudflare; docs reais em docs.clickbank.net (SPA; dados em /docs/*.json) e em support.clickbank.com/en/articles/10535400 (ClickBank APIs)
  • QuickStats API (GET /rest/1.3/quickstats/accounts|count|list) — lista nicknames de contas com acesso do API user + totais diários sale/refund/chargeback; quickstats/accounts revela o nickname necessário para o parâmetro account da Analytics API. Role necessária: API Order Read
  • Namespace /2.0/* de api.clickbank.com retorna 403 no nginx mesmo sem auth (bloqueado no gateway)
  • Sandbox (sandbox.clickbank.com) descontinuado
  • Keys em ~/.brandvelia/clickbank-api.json (local, 600) → env vars CLICKBANK_API_KEY_1 / CLICKBANK_API_KEY_2 no Vercel

Email (fase 2)

  • Unsubscribe público: /unsubscribe?token=… → POST /api/email/unsubscribe (revoga consentimento, registra email_events, atualiza lead)
  • Platform é source of truth; ESP (envio real) será adapter quando um provedor for escolhido

Comandos úteis

# DNS (token em ~/.cloudflare/brandvelia-dns-token)
TOKEN=$(cat ~/.cloudflare/brandvelia-dns-token)
curl -s "https://api.cloudflare.com/client/v4/zones/f5424150e857d92628718facac2fdf42/dns_records" \
  -H "Authorization: Bearer $TOKEN" | jq -r '.result[] | "\(.type) \(.name) -> \(.content)"'

# Vercel
vercel ls                    # deployments
vercel env ls                # variáveis
vercel logs <deployment-url> # logs

# Supabase
supabase db push --linked --dry-run   # preview do que será aplicado
supabase migration list --linked      # estado local vs remoto

Cron & schedulers

O plano Hobby da Vercel só permite expressões cron diárias (expressões mais frequentes falham o deploy com "Hobby accounts are limited to daily cron jobs"), então o agendamento é dividido:

Agendador Cadência Alvos
Vercel Cron (apps/web/vercel.json) Diário, escalonado (UTC) marketplace-sync 00:35 · quickstats-sync 01:15 · google-conversions 01:45 · google-campaigns 02:25 · notifications 02:55 · traffic-control 03:10 (rede de segurança) · offer-decisions 03:40
Servidor Linux do operador (systemd, scripts/linux/) Alta frequência google-campaigns a cada 5 min · traffic-control a cada 15 min — brandvelia-cron@.service lendo /etc/brandvelia/cron.env; guia em scripts/linux/README.md (seção "Crons no servidor")
Agendador externo (cron-job.org, gratuito, até 60 exec/h) Alta frequência marketplace-sync + quickstats-sync 2x/dia (06:20 e 18:20 UTC) · google-conversions a cada 10 min · notifications a cada 15 min — todos via GET + Authorization: Bearer $CRON_SECRET
GitHub Actions (.github/workflows/cron.yml) Somente manual (workflow_dispatch) fallback dos quatro jobs acima

Um dono por job. Cada job de alta frequência tem um único agendador; o Vercel Cron diário é só a base. Chamar a mesma rota por dois agendadores não duplica efeito (as rotas são replay-safe), mas dobra as leituras na API do Google e confunde o diagnóstico.

Job Dono Cadência Base (Vercel, UTC)
google-campaigns systemd (servidor Linux) */5 02:25
traffic-control systemd (servidor Linux) */15 03:10
google-conversions cron-job.org */10 01:45
notifications cron-job.org */15 02:55
marketplace-sync cron-job.org 06:20 e 18:20 UTC 00:35
quickstats-sync cron-job.org 06:20 e 18:20 UTC 01:15
offer-decisions Vercel Cron (opcional: systemd 06:40 UTC, logo após a captura) diário 03:40

Os timers brandvelia-cron-google-conversions.timer e brandvelia-cron-notifications.timer existem em scripts/linux/, mas não devem ser habilitados enquanto o cron-job.org for o dono desses jobs. Para trocar o dono, desative primeiro o job no cron-job.org e só então habilite o timer.

Avisos no Telegram

  • Bot: TELEGRAM_BOT_TOKEN (segredo, Vercel) e TELEGRAM_BOT_USERNAME (sem @; se vazio o app usa getMe). O card em Conexões mostra o @bot, o chat vinculado (mascarado), "Último aviso entregue", a fila, as falhas de 7 dias e o botão Enviar teste (entrega só a mensagem de teste daquele workspace). O item do checklist só fica pronto depois de 1 entrega real.
  • TELEGRAM_WEBHOOK_SECRET fica só para o vínculo por /start (PR seguinte). Atenção: setWebhook desliga o getUpdates de qualquer outro consumidor do mesmo token.
  • Outbox com lease: bv_claim_notification_batch reserva as linhas por 6 min (claimed_until ≥ 20 envios × 15 s de timeout + margem) com um claim_token; só o dono do lease finaliza (bv_finish_notification devolve false se o lease foi perdido e o app loga NOTIFICATION_LEASE_LOST). notifications, traffic-control, google-campaigns, "Pausar tudo" e "Enviar teste" podem entregar no mesmo minuto sem duplicar mensagem. 400/403 → FAILED; 429/5xx/rede → RETRY (até 10 tentativas); 401/404 → RELEASE sem gastar tentativa. Se a leitura dos vínculos (bv_channel_bindings) falhar, o lote inteiro volta com RELEASE e a rota responde BINDINGS_LOAD_FAILED — nunca marca NO_BINDING às cegas.
  • Avisos de operação (sem workspace, ex.: INS sem contrato) vão só para os workspaces em site_publishing_workspaces com chat vinculado.
Fonte Chave de dedup Frequência máxima
Pausa pedida (guarda / Pausar tudo) spend-guard:<campanha>:<regra>:<data>:<comando> 1 por comando (repete no mesmo dia se houver novo ciclo)
Pausa confirmada no Google gcmd-pause-ok:<comando> 1 por comando
Pausa em RETRY (1ª tentativa) gcmd-pause-retry:<comando> 1 por comando
Comando em DLQ (PAUSE sempre; outros tipos se não for erro de acesso) gcmd-dlq:<comando>:<data>[:r<n>] 1 por comando/dia e por ciclo de requeue
Pausa sem resultado há > 15 min (desde o pedido ou o último requeue) gcmd-pause-stale:<comando>:<data>[:r<n>] 1 por comando/dia e por ciclo
Guarda cego com campanha no ar guard-blind:<data>T<bloco de 3 h> a cada 3 h
Conexão inativa / payout desconhecido / sem cotação / cotação ≥ 25 dias guard-warning:<código>:<data> 1 por dia
Token/permissão do Google Ads (WP2, note_google_api_health) 1 por dia por conexão
Sync de gasto do Google (erro que não é de acesso) google-spend:<conexão>:<data> 1 por dia por conexão
INS FAILED / teste INS recebido ins-failed:<evento>:<código> / ins-test:<evento> 1 por evento
QuickStats FAILED quickstats-failed:<conta>:<data> 1 por conta por dia
OCI: venda sem click ID oci-blocked:<data> 1 por dia por workspace
OCI: DLQ que não é de acesso (acesso vai para a saúde do WP2) oci-dlq:<conexão>:<data> 1 por conexão por dia
OCI: enqueue com motivo inesperado oci-enqueue:<motivo>:<data> 1 por motivo por dia
Agente de captura mudo / execução travada capture-agent-silent:<agente>:<data> / capture-agent-hung:<agente>:<data> 1 por agente por dia

As datas das chaves são a data local do operador (America/Sao_Paulo). traffic-control responde ok:false com o guarda cego (leitura ao vivo falhou com campanha no ar, ou o contexto do guarda não pôde ser lido — CONTEXT_LOAD_FAILED, com aviso a cada 3 h) ou quando a lista de workspaces falha (GUARD_WORKSPACES_LOAD_FAILED), e só entrega o outbox quando houve pausa ou aviso novo. google-campaigns entrega quando houve DLQ ou resultado de PAUSE.

Como cadastrar no cron-job.org

  1. Criar conta gratuita e abrir Jobs → Create cronjob.
  2. Para cada job abaixo: URL = https://brandvelia.com/api/cron/<job>, Request method = GET, Request headers = Authorization: Bearer <CRON_SECRET> (mesmo valor da env var CRON_SECRET na Vercel), Schedule conforme a tabela, Request timeout = 300 s (rotas têm maxDuration = 300). google-campaigns e traffic-control não ficam aqui: rodam no servidor Linux (timers systemd).
Título sugerido URL Schedule
bv marketplace-sync /api/cron/marketplace-sync 2x/dia 06:20, 18:20 UTC
bv quickstats-sync /api/cron/quickstats-sync 2x/dia 06:20, 18:20 UTC
bv google-conversions /api/cron/google-conversions */10 * * * *
bv notifications /api/cron/notifications */15 * * * *
  1. Usar Test run para validar cada job (esperado: 200 {"ok":true,...} sem vazar segredos) e ativar status notifications para saber de falhas.
  2. Nunca colocar o CRON_SECRET na URL — sempre como header Authorization.

Regras:

  • Toda rota /api/cron/* exige Authorization: Bearer $CRON_SECRET (401 caso contrário). O secret precisa existir tanto nas env vars da Vercel quanto nos secrets do repositório GitHub.
  • O workflow do GitHub precisa de dois secrets: CRON_SECRET e SITE_URL (URL pública do app).
  • google-campaigns faz duas coisas: (1) ingere o gasto diário oficial por campanha do Google Ads via GAQL (janela D-7..D-1, revisões aplicadas pela RPC bv_ingest_google_api_spend_v2 e upsert idempotente em bv_transactions por (workspace_id, external_ref)) e (2) entrega comandos pendentes de pausa/criação/ativação/orçamento. O sync de spend roda mesmo sem comandos.
  • notifications entrega o outbox (venda/rebill/refund/chargeback/falha) via Telegram; requer TELEGRAM_BOT_TOKEN e o chat vinculado em Conexões → Telegram (channel_bindings). Não existe mais TELEGRAM_CHAT_ID: workspace sem vínculo fica com o aviso FAILED NO_BINDING. Responde ok:false sem token (TELEGRAM_BOT_TOKEN_MISSING), com token recusado pelo Telegram (401/404 → TELEGRAM_BOT_TOKEN_INVALID; o lote para e os avisos continuam PENDING sem gastar tentativa) ou quando nada foi entregue e houve falha ou retry. Workspace sem chat (NO_BINDING) é contado à parte (noBinding) e não derruba o ok. Veja "Avisos no Telegram".
  • traffic-control é o controle de gasto (Meu orçamento): lê do Google o status, o orçamento e o custo de HOJE (fuso da conta) de cada campanha e pausa sozinho — com aviso no Telegram — quando "Pausar tudo" está ligado, a verba de teste acabou (reserva de 1 dia), o gasto do dia passou do teto ou uma campanha gastou o stop-loss (× comissão, convertido pela cotação) sem venda líquida (sem cotação do dólar, o stop-loss só avisa). Roda a cada 15 min no servidor Linux (timer systemd): o diário da Vercel é só a rede de segurança. O custo do Google chega com ~3 h de atraso; a proteção principal é o bloqueio na ativação (Σ orçamentos ≤ teto).
  • O agente de captura (servidor Linux do operador) envia para POST /api/agent/capture/import e /report com o token bvcap_… gerado em Conexões (guardado só como hash). POST /api/cron/marketplace-capture-import continua aceito para agentes antigos (CRON_SECRET, só com um workspace). Guia: docs/operations/capture-agent-linux.md.
  • marketplace-sync também é o vigia do agente: agente sem sinal há mais de 26 h ou captura iniciada sem fim há mais de 90 min gera aviso; execuções running há mais de 2 h viram failed. Continua fazendo o housekeeping de retenção (snapshots com mais de 90 dias). Sem MARKETPLACE_FEED_URL ele não grava mais runs skipped_no_source.
  • Os syncs são incrementais e replay-safe (watermarks/run keys); rodá-los mais de uma vez por período é inofensivo.