0017 — Metabase signed embedding (supersede o "BI = só link" da 0014)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-06 · Decidido em: 2026-07-06
Contexto¶
A decisão 0014 §6 fixou "BI = só link": o auth devolve
metabase_dashboard_id e a Central abre o dashboard numa aba — sem embutir. O motivo era evitar
superfície: signed embedding exige um embedding secret no env do auth e um iframe no painel.
O trabalho de padronização da Central (authui, .ia/003, L9) pediu o dashboard embutido no
detalhe do app (melhor UX que pular pra outra aba). Isso reverte o §6 da 0014 — uma decisão
explícita (REGRA Nº 3), registrada aqui.
Decisão¶
Adotar signed embedding do Metabase, mantendo o link como fallback. Três peças no auth:
MetabaseProviderhabilita embedding no dashboard (A2): ao provisionar/reusar,GETo dashboard e, seenable_embeddingnão étrue, fazPUT /api/dashboard/:id <!-- checa-rotas: ignorar --> {enable_embedding:true}(idempotente, atualização parcial). Sem o flag, o Metabase recusa/embed/dashboard/<jwt>com 400. Best-effort e por último (depois dos cards): se o Static Embedding está off na instância (o default do A0), oPUT400a — a falha é engolida comWARN, não derruba a provisão. Degrada pra "dashboard com cards, sem embed" (a Central usa o link simples), nunca "dashboard vazio". Antes oenable_embeddingrodava antes dos cards e era fatal → numa instância sem A0 a provisão estourava com o dashboard já criado e vazio.MetabaseEmbedSignerminta a URL de embed assinada (A2): JWT HS256 com oMETABASE_EMBEDDING_SECRET(payload{resource:{dashboard:<id>}, params:{}, exp}), no mesmo padrão do token forjado do Aptabase; a URL é<metabase>/embed/dashboard/<jwt>#bordered=true&titled=true, mintada em read-time (TTL curto, default 10 min) peloAppCatalogControllere exposta emAppLinks.metabase_embed, só quando o providermetabaseestáligada.- Links contextuais resolvidos server-side (A1): o
AppCatalogControllertambém resolveAppLinks.glitchtip(<glitchtip>/<org>/<slug>) eAppLinks.metabase(<metabase>/dashboard/<id>) dos refs persistidos — a UI não deriva URL de slug/repo (o DSN do GlitchTip não carrega o slug).
Graceful sem A0: enquanto o METABASE_EMBEDDING_SECRET não estiver no env, o signer devolve
empty → metabase_embed é null → a Central cai no link simples. Ou seja, este código é
deployável antes do A0 (habilitar signed embedding no Metabase + prover o secret): acende sozinho
quando o secret aparecer.
Justificativa da superfície (que a 0014 evitava)¶
- Embedding secret no env do auth: é do mesmo tipo do
METABASE_API_KEYe doAPTABASE_AUTH_SECRETque o auth já guarda (secrets de provider, cifrados no Coolify) — não é uma classe nova de risco. O JWT de embed é read-only, escopado a um dashboard, com TTL curto (≠ o api-key de escrita). - Iframe no painel admin: o embed roda só no painel admin (
/api/admin/apps, atrás doAdminAuthFilter+ JWTxadm_admin), não numa superfície pública. O app do usuário final não embeda (continua no link). - Só-web: o
HtmlElementView(iframe) é do lado authui, condicional akIsWeb(fallback link fora do web). O auth só entrega a URL.
Consequências / riscos¶
- Verificação real pendente do A0. O
enable_embeddingPUT e a URL assinada não foram exercitados contra um Metabase real (exige o A0: signed embedding habilitado + o secret). O contrato do payload de embed (resource/params/exp, HS256) segue a doc do Metabase; reconferir ao habilitar o A0. Cobertura atual: unit test do signer (assinatura + graceful sem secret) + WireMock doenable_embeddingPUT (habilita quando ausente e degrada não-fatal quando a instância recusa com 400, com os cards presentes) + teste de resolução de links (A1). - Rotação do embedding secret invalida as URLs em voo (TTL curto → impacto pequeno).
- Versão do Metabase: signed embedding é estável há muitas versões; o guard de versão da 0014 (API keys ≥ 0.49) já cobre o mínimo.
Operar (config no recurso auth)¶
Além dos envs da 0014:
| Variável | Valor | Secret? |
|---|---|---|
METABASE_EMBEDDING_SECRET |
Admin → Settings → Embedding → Static embedding → Embedding secret key | sim ✅ |
METABASE_EMBED_TTL_SECONDS |
TTL da URL assinada (default 600) |
não |
A0 (infra, uma vez): Admin → Settings → Embedding → habilitar Static embedding e copiar a secret key pro env do auth. Sem isso, a Central usa o link simples (graceful).
Alternativas consideradas¶
- Manter "só link" (0014): pula pra outra aba — UX pior; o pedido L9 é embutir.
- Public embedding (dashboard público, sem assinatura): expõe os dados sem controle — recusado.
- Interactive / SSO embedding: fora de escopo (mais integração; o static embedding basta).
- Mintar a URL no provision (persistida): o JWT tem
exp— persistir venceria. Mintar em read-time resolve.