Marketing (Hub framework — paid only)
Status: 🟢 Estável (Hub.1 framework + Hub.2 Meta Ads + Hub.4 Google Ads) Code: backend/app/modules/marketing UI: frontend/src/app/(admin)/[slug]/admin/marketing Última revisão deste doc: 2026-05-13 por Felipe + Claude Dependências fortes: attribution (campaign bridge), tenant_router, billing-crypto (envelope cifragem)
1. Identidade
O que faz (uma frase)
Hub canônico de marketing pago — plugin framework com 4 verbos (Observe / Operate / Predict / Suggest) onde cada provider (Meta Ads, Google Ads — e plugins externos GA4/YouTube/Instagram que vivem aqui) registra implementação isolada, com credenciais cifradas + audit completo de operações sensíveis.
Por que existe (negócio)
Mandir precisa de visão consolidada de gasto pago + orgânico, sem Felipe abrir 4 painéis diferentes. Antes deste módulo:
- Spend Meta Ads ficava no Ads Manager.
- Performance Google Ads no Google Ads Console.
- GA4 no painel próprio.
- YouTube no Studio.
Sem cruzamento → nenhuma decisão integrada.
Marketing Hub centraliza:
- Observe (sempre) — persistir L0 dos providers em tabelas
<provider>_*no Postgres do tenant. - Operate (com policy) — pausar campanha, ajustar bid, etc. (com aprovação humana ou autopilot).
- Predict / Suggest — previsão de spend e recomendações (Hub.6+ futuro).
Tools pro Conselho (4 GA4 + 6 YouTube + 7 paid total) consultam direto sem o conselheiro saber detalhes da API.
Importante (memória [[council-scope-split-marketing]]): após split em 2026-05-13, o scope marketing cobre apenas paid (Meta Ads + Google Ads). Analytics (GA4) virou namespace analytics; Instagram orgânico virou instagram; YouTube virou youtube.
Por que existe (técnico)
- Plugin architecture — adicionar provider novo = pasta nova + register, sem mexer core.
- Persiste L0 (memória [[mandir-hub-arquitetura-persistir]]) — todo plugin grava em tabelas
<provider>_*no Postgres do tenant. Conselheiros/UI lêem do DB. Latência 10ms vs 500ms hitting API; sem quota; histórico; cruzamento SQL. - Credenciais cifradas —
marketing_credential.secret_envelopeJSONB (AES-256-GCM, KEK por tenant via HMAC). - Operate com policy + audit —
marketing_operate_policydefinemode(dry_run / confirm_required / autopilot),daily_spend_cap_brl,allowed_actions. Toda operação emmarketing_operate_audit(append-only, antes/depois state).
Status atual
- Hub.1 framework + Hub.2 Meta Ads + Hub.3 GA4 + Hub.4 Google Ads + Hub.5 Instagram + Hub.6 YouTube todos deployados em 2026-05-13. Memórias [[sessao-2026-05-13-hub234]] + [[sessao-2026-05-13-hub56]].
- 5 plugins ativos: ga4, google_ads, instagram, meta_ads, youtube.
Próxima mudança: Hub.6+ para Operate de Google Ads (pausar campanhas, ajustar bid). Hub.7+ Predict + Suggest.
2. Cases de uso reais
Case 1: Conselheiro CEO consulta paid media summary
Fluxo: CEO chama tool read_paid_media_summary(window_days=30) → cross_module agrega meta_ads_insight_daily + google_ads_insight_daily → retorna {spend_brl, impressions, clicks, conversions, ROI} → CEO inclui na resposta.
Case 2: Sync diário Meta Ads
Beat task marketing.run_observe(provider=meta_ads) → resolve credenciais ativas → para cada uma, decifra → call Meta Marketing API (campaigns + adsets + ads + insights chunked por dia) → upsert em meta_ads_* → registra marketing_sync_run(status=ok, rows_ingested).
Case 3: Felipe pausa anúncio (Operate)
UI /admin/marketing/meta-ads → botão "Pausar" → OperateRequest(action=PAUSE, action_qualified="meta_ads.pause_ad", payload={ad_id, ...}) → consulta marketing_operate_policy → se mode=confirm_required, awaits aprovação → executa via Meta API → registra marketing_operate_audit (state_before/after, executed_at, result).
3. Oportunidades de negócio
- Cross-channel attribution as a service: consolidar attribution_campaign cross-providers — recurso premium pra agências.
- Operate-as-a-service: "pausar tudo se ROAS < 1.0" automático com policy autopilot — diferencial sobre Ads Manager.
- Spend forecasting: Predict via Claude com histórico → projeção semanal de gasto.
- Plugin marketplace: terceiros desenvolvem plugins (TikTok Ads, LinkedIn Ads, Pinterest, etc.) com revenue share.
- Compliance LGPD nativa: auditing completo (audit log) + opt-out por contato → produto pra setores regulados.
Riscos: providers mudam APIs frequentemente (Google Ads v18 → v19 anual; Meta v22 → v23); credenciais podem ser revogadas; rate limits.
4. Arquitetura interna
Arquivos
| Arquivo | Propósito |
|---|---|
hub.py | Contrato HubPlugin + registry + helpers |
models.py | 4 tabelas core |
service.py | get_effective_policy, evaluate_operate, audit |
credentials.py | store_credential, load_credential_secret, mark_used/error/refreshed |
tasks.py | run_observe, run_observe_all, backfill workers |
routes.py | 34 endpoints /api/marketing/* |
plugins/bootstrap.py | load_plugins() no startup + worker |
plugins/<provider>/ | Cada plugin: plugin.py, client.py, models.py, sync.py, attribution_bridge.py |
Plugin contract
class HubPlugin(Protocol):
provider: str # "meta_ads", "google_ads", etc
async def observe(db, *, tenant_id, credential_id, window) -> ObserveResult
async def operate(db, *, tenant_id, credential_id, request) -> OperateResult
async def predict(db, *, tenant_id, scope) -> list[Forecast]
async def suggest(db, *, tenant_id) -> list[Recommendation]
Registry PLUGIN_REGISTRY: dict[str, HubPlugin] — register_plugin(plugin) no __init__.py de cada subpasta.
Tasks Celery
| Task | Trigger | Idempotência |
|---|---|---|
marketing.run_observe | On-demand (POST /sync/run) | marketing_sync_run audit |
marketing.run_observe_all | Beat | Itera tenants ativos × plugins × credenciais ativas |
marketing.{provider}_backfill | On-demand | Chunked (Meta 7d, Google 30d) |
marketing.instagram_stories_capture | Beat 1h | Stories expiram 24h |
5. Tabelas + relacionamentos
Tabelas core (4)
marketing_credential
| Coluna chave | Notas |
|---|---|
tenant_id (idx) | |
provider | meta_ads / google_ads / ga4 / youtube / instagram |
label | Humano-readable |
account_external_id | Ad account ID, customer_id, property_id, channel_id |
auth_kind | oauth2 / api_key / service_account |
secret_envelope (JSONB) | Cifrado AES-256-GCM via core.billing_crypto |
scope | OAuth scopes |
expires_at / refresh_at | OAuth tracking |
is_active | Soft kill |
last_error / last_used_at | Audit |
Constraint: UNIQUE(tenant_id, provider, account_external_id).
marketing_sync_run
| Coluna chave | Notas |
|---|---|
provider / credential_id | |
kind | incremental / backfill / manual / health_probe |
started_at / finished_at / duration_ms | |
status | running / ok / failed / partial |
rows_ingested | |
error | |
cursor_after | Checkpoint pra continuação |
marketing_operate_policy
| Coluna chave | Notas |
|---|---|
provider | * = default global ou específico |
mode | dry_run / confirm_required (default fail-safe) / autopilot |
daily_spend_cap_brl / per_action_spend_cap_brl | Limites |
allowed_actions / blocked_actions | JSONB list |
Resolução: provider específico > * > fail-safe (confirm_required + caps NULL + listas vazias).
marketing_operate_audit
Append-only.
| Coluna chave | Notas |
|---|---|
provider / credential_id / action | ex: meta_ads.pause_ad |
actor_kind | user / council / agent / system |
actor_id / approved_by_user_id / approved_at | |
payload_request / state_before / state_after (JSONB diff) | |
external_response | Resposta provider |
estimated_spend_impact_brl | |
result | pending / ok / failed / reverted / denied |
error / executed_at |
Tabelas L0 por plugin
- Meta Ads (6):
meta_ads_account,meta_ads_campaign,meta_ads_adset,meta_ads_ad,meta_ads_insight_daily,meta_ads_audience. - Google Ads (6):
google_ads_customer,google_ads_campaign,google_ads_ad_group,google_ads_keyword,google_ads_insight_daily,google_ads_search_term_daily. - GA4 (4): ver analytics.
- YouTube (9): ver youtube.
- Instagram (6): ver instagram (compartilhado, não tabelas
ig_*pra evitar colisão).
Relacionamentos cross-módulo
| Direção | Outro módulo | Como |
|---|---|---|
| ↘ Escreve | attribution | attribution_bridge.sync_attribution_campaigns cria attribution_campaign por novo Meta/Google ad |
| ↘ Tools | council | 7 tools paid (read_paid_media_summary, read_meta_*, read_google_ads_*) |
| ↘ Emite event | tracking | marketing.sync.*, marketing.operate.* |
6. API / Endpoints (34)
Prefixo /api/marketing.
Hub Infrastructure (6)
| Método | Rota | O que faz |
|---|---|---|
| GET | /plugins | Lista plugins registrados |
| GET | /health | Plugins, credenciais por provider, last sync, pending ops |
| GET | /credentials | Lista (filtro provider) |
| POST | /credentials | Store (cifra secret) |
| POST | /credentials/{id}/deactivate | Mark inactive |
| GET / PUT | /operate-policy | CRUD policy |
Sync Triggers (8)
| Método | Rota | O que faz |
|---|---|---|
| POST | /sync/run | Generic observe (provider, credential_id, window_days) |
| POST | /sync/{provider}/backfill | Chunked backfill (meta-ads, google-ads, ga4, youtube, instagram) |
| POST | /sync/instagram/stories | Capture stories (cadência 1h) |
| GET | /sync-runs | Histórico runs |
Views por provider
- Meta Ads:
/meta-ads/campaigns,/meta-ads/summary(spend 30d). - Google Ads:
/google-ads/summary,/google-ads/campaigns,/google-ads/search-terms. - GA4:
/ga4/summary,/ga4/sources,/ga4/events(ver analytics). - YouTube:
/youtube/{summary,videos,traffic-sources,geography,demographics,search-terms}(6 endpoints — ver youtube). - Instagram:
/instagram/{summary,media,audience,hashtags}(4 endpoints — ver instagram).
Audit (1)
| Método | Rota | O que faz |
|---|---|---|
| GET | /operate-audit | Histórico (filtro provider, resultado, actor) |
7. Tools no Council (paid only após split)
| Tool | Implementação |
|---|---|
read_paid_media_summary | Agrega meta_ads_insight_daily + google_ads_insight_daily |
read_meta_top_ads | Top ads por spend/CTR/conversões |
read_meta_campaign_insights | Detalhe por campanha |
read_google_ads_summary | Spend BRL, conversions, ROI 30d |
read_google_ads_campaigns | List com cost_brl, CPC, conversions |
read_google_ads_search_terms | Top search terms por conversão |
read_google_ads_keywords | Keywords com quality_score, bid |
GA4/YouTube/Instagram tools movidos pros respectivos módulos após split.
8. Configuração
Env vars
| Var | Propósito |
|---|---|
MANDIR_CRYPTO_MASTER_KEY | Obrigatório — KEK para secret_envelope |
MANDIR_PLATFORM_DATABASE_URL | Multi-tenant |
| Provider-specific tokens | Em secret_envelope por credencial (não env) |
Kill switches
- Policy
mode=dry_run→ nega todas as operações (teste). credential.is_active=false→ skip observe.- Pause global: dropar/desativar credencial.
Rate limits
- Google Ads API: 100k requests/mês padrão.
- Meta Graph: ~200 calls/min.
- GA4 Data API: 1MM requests/dia/property.
- YouTube Analytics: 500M units/dia/projeto.
9. Operações
Como conectar provider novo
- UI
/admin/marketing→ "Conectar {Provider}" → OAuth flow ou paste API key. - Backend:
store_credential(tenant_id, provider, label, secret)→ cifra envelope. - Trigger inicial sync:
POST /sync/run?provider=X&credential_id=Y. - Verifica
marketing_sync_run.status=ok+rows_ingested>0.
Troubleshooting
Sintoma: Sync falha com auth_error
Diagnóstico: SELECT last_error FROM marketing_credential WHERE id=.... OAuth refresh expirou ou revogado.
Fix: UI reconectar → novo refresh_token → mark_token_refreshed.
Sintoma: Operate negado mesmo em autopilot
Causa: per_action_spend_cap_brl excedido pela operação.
Fix: ajustar policy ou aprovar manualmente.
10. Limitações e débitos técnicos conhecidos
| # | Item | Impacto |
|---|---|---|
| 1 | Google Ads API versionada | Update anual obrigatório (v18 → v19) |
| 2 | Service Account vs OAuth Client confusão | Memória [[oauth-client-vs-service-account-json]] documenta |
| 3 | MIGRATION_DATABASE_URL hardcoded em alguns workflows | Memória [[sessao-2026-05-13-hub234]] |
| 4 | Meta Cloud API page_size limitations | Adapter chunked |
| 5 | parse_iso em datetimes inconsistente entre providers | Helper unificado em service.py |
| 6 | Operate sem rollback automático | Manual via audit (state_before) |
| 7 | Sem circuit breaker cross-provider | Fase pós Hub.4 |
11. Histórico relevante
- 2026-05-13 — Hub.1 + Hub.2 + Hub.3 + Hub.4 + Hub.5 + Hub.6 todos em prod. Memórias [[sessao-2026-05-13-hub234]] + [[sessao-2026-05-13-hub56]].
- 2026-05-13 — Split scope
marketingem 4 namespaces (marketingpaid +analyticsGA4 +instagramorgânico+DM +youtube). Commitd28b54d. Memória [[council-scope-split-marketing]]. - 2026-05-13 — Projeto GCP
mais-consciente-mandircravado: OAuth Client + 7 scopes + 5 APIs ativas + SAmais-consciente-analytics.
12. Glossário
- Hub plugin: implementação de
HubPlugin(Observe/Operate/Predict/Suggest). - L0: dados crus persistidos em tabelas
<provider>_*(não derivados). - Observe: persistir dados; sempre seguro.
- Operate: executar ação no provider (pausar, ajustar); sujeito a policy.
- Predict: projeção (futuro Hub.6+).
- Suggest: recomendação (futuro Hub.7+).
- Policy mode:
dry_run(nega tudo) /confirm_required(default — humano aprova) /autopilot(executa direto se dentro de allowed_actions + caps).