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 embrandvelia.comeapp.brandvelia.com. - Protection: URLs
*.vercel.appexigem 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
panoptesatende outros sistemas em produção (Finanças, Conexao, Radar, Viagens, Z900, Panoptes/ações). Nunca alterar/dropar objetos fora do schemabrandvelia. - 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.sqlemsupabase/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
brandveliacom RLS habilitado (isolamento porworkspace_idviaworkspace_members). - A API REST as acessa por views auto-atualizáveis
public.bv_*comsecurity_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 embv_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+campaignnomeado; 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órioaccount(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.combloqueado por Cloudflare; docs reais emdocs.clickbank.net(SPA; dados em/docs/*.json) e emsupport.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/accountsrevela o nickname necessário para o parâmetroaccountda Analytics API. Role necessária: API Order Read - Namespace
/2.0/*deapi.clickbank.comretorna 403 no nginx mesmo sem auth (bloqueado no gateway) - Sandbox (
sandbox.clickbank.com) descontinuado - Keys em
~/.brandvelia/clickbank-api.json(local, 600) → env varsCLICKBANK_API_KEY_1/CLICKBANK_API_KEY_2no Vercel
Email (fase 2)¶
- Unsubscribe público:
/unsubscribe?token=…→ POST/api/email/unsubscribe(revoga consentimento, registraemail_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) eTELEGRAM_BOT_USERNAME(sem @; se vazio o app usagetMe). 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_SECRETfica só para o vínculo por/start(PR seguinte). Atenção:setWebhookdesliga ogetUpdatesde qualquer outro consumidor do mesmo token.- Outbox com lease:
bv_claim_notification_batchreserva as linhas por 6 min (claimed_until≥ 20 envios × 15 s de timeout + margem) com umclaim_token; só o dono do lease finaliza (bv_finish_notificationdevolvefalsese o lease foi perdido e o app logaNOTIFICATION_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 →RELEASEsem gastar tentativa. Se a leitura dos vínculos (bv_channel_bindings) falhar, o lote inteiro volta comRELEASEe a rota respondeBINDINGS_LOAD_FAILED— nunca marcaNO_BINDINGàs cegas. - Avisos de operação (sem workspace, ex.: INS sem contrato) vão só para os
workspaces em
site_publishing_workspacescom 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¶
- Criar conta gratuita e abrir Jobs → Create cronjob.
- Para cada job abaixo: URL =
https://brandvelia.com/api/cron/<job>, Request method =GET, Request headers =Authorization: Bearer <CRON_SECRET>(mesmo valor da env varCRON_SECRETna Vercel), Schedule conforme a tabela, Request timeout = 300 s (rotas têmmaxDuration = 300).google-campaignsetraffic-controlnã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 * * * * |
- Usar Test run para validar cada job (esperado:
200 {"ok":true,...}sem vazar segredos) e ativar status notifications para saber de falhas. - Nunca colocar o
CRON_SECRETna URL — sempre como headerAuthorization.
Regras:
- Toda rota
/api/cron/*exigeAuthorization: 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_SECRETeSITE_URL(URL pública do app). google-campaignsfaz 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 RPCbv_ingest_google_api_spend_v2e upsert idempotente embv_transactionspor(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.notificationsentrega o outbox (venda/rebill/refund/chargeback/falha) via Telegram; requerTELEGRAM_BOT_TOKENe o chat vinculado em Conexões → Telegram (channel_bindings). Não existe maisTELEGRAM_CHAT_ID: workspace sem vínculo fica com o avisoFAILED NO_BINDING. Respondeok:falsesem token (TELEGRAM_BOT_TOKEN_MISSING), com token recusado pelo Telegram (401/404 →TELEGRAM_BOT_TOKEN_INVALID; o lote para e os avisos continuamPENDINGsem gastar tentativa) ou quando nada foi entregue e houve falha ou retry. Workspace sem chat (NO_BINDING) é contado à parte (noBinding) e não derruba ook. 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/importe/reportcom o tokenbvcap_…gerado em Conexões (guardado só como hash).POST /api/cron/marketplace-capture-importcontinua aceito para agentes antigos (CRON_SECRET, só com um workspace). Guia:docs/operations/capture-agent-linux.md. marketplace-synctambé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çõesrunninghá mais de 2 h viramfailed. Continua fazendo o housekeeping de retenção (snapshots com mais de 90 dias). SemMARKETPLACE_FEED_URLele não grava mais runsskipped_no_source.- Os syncs são incrementais e replay-safe (watermarks/run keys); rodá-los mais de uma vez por período é inofensivo.