Seu SaaS quer um ecossistema de plugins. Web Components ou iframes? Um playbook para CTOs

Por Diogo Hudson Dias
Front-end engineer in a São Paulo office reviewing a plugin running in a sandboxed iframe via Chrome DevTools, with security and network panels open.

Você não tem segunda chance em um ecossistema de plugins. Na primeira vez que uma extensão ler os dados do tenant errado ou gravar chaves no seu localStorage, você comprou um trimestre perdido, um ciclo de PR e um déficit de confiança que vai carregar por anos. Um post técnico recente, “Web Components vs. Iframes: A Hard Lesson in DOM Isolation Barriers”, ganhou tração porque expôs uma verdade incômoda: Shadow DOM não é uma barreira de segurança. Se você está construindo um marketplace ou uma superfície de “extensões” em 2026, trate composição de UI e isolamento de segurança como decisões separadas — porque o navegador faz isso.

O problema que você realmente está resolvendo

“Permitir que terceiros estendam nosso app” parece uma coisa só, mas na prática são quatro problemas com soluções incompatíveis:

  • Execução de código não confiável: Um plugin consegue rodar JS arbitrário sem exfiltrar dados ou quebrar o host?
  • Integração com o DOM: Um plugin consegue desenhar dentro da sua UI sem implodir seu design system?
  • Escopo de dados e capacidades: Um plugin consegue fazer somente o que o usuário e o tenant autorizam — e nada além disso?
  • Isolamento de performance: Você consegue impedir que um plugin ruim afunde INP, TTI ou memória para todos?

Misture isso e você escolherá o primitivo errado. É assim que times acabam usando Web Components para trabalho de segurança que eles não conseguem cumprir, ou iframes inchando uma página que não precisava deles.

A resposta curta

  • Se você um dia permitirá desenvolvedores terceirizados que você não emprega diretamente, use iframes cross-origin para isolamento de execução. Ponto.
  • Use Web Components (Shadow DOM) para blocos de UI first-party ou de parceiros validados em que você precisa de integração pesada com o DOM — não para sandboxing.
  • Empurre lógica pesada ou de longa duração de plugins para Web Workers e converse via canais de mensagem. Workers não são uma barreira de segurança, mas mantêm a thread principal saudável.
  • Empacote tudo em APIs do host com escopo de capacidades. Nunca passe handles crus de window, document ou storage para o código do plugin.

Esse é o esqueleto. Os detalhes abaixo levam isso a nível de produção.

Por que Web Components não são o seu sandbox

Shadow DOM e custom elements resolvem vazamento de CSS e eventos. Eles não resolvem proteção de dados entre tenants. Um plugin malicioso ou simplesmente com bug em um Web Component pode:

  • Ler qualquer estado acessível por JS que você entregar a ele (props, context, tokens).
  • Chamar fetch com suas credenciais ambientes se você lhe der acesso same-origin às suas APIs.
  • Exfiltrar via qualquer callback aberto do host que receber.

Sim, dá para apertar com Trusted Types, CSP estrita e limites de tipos rigorosos. Mas se o desenvolvedor está fora da sua organização — e seu modelo de ameaça inclui “o plugin tenta fazer mais do que deveria” — Web Components servem para composição, não para contenção.

Por que iframes cross-origin são o padrão chato e correto

Iframes são segurança barata. O navegador gastou duas décadas os fortalecendo. Os primitivos de isolamento em que você pode confiar em 2026:

  • Origin boundary: Origem diferente, credenciais ambientes diferentes. Cookies de terceiros estão sendo descontinuados; aqui isso é uma vantagem.
  • Sandbox attribute: Remova allow-same-origin e o iframe vira uma origem opaca. Conceda só o que precisa: allow-scripts, allow-forms, talvez allow-popups-to-escape-sandbox para OAuth. Desabilite microfone, câmera, USB, pagamentos por padrão.
  • Permissions Policy: Negue APIs poderosas por iframe. Sem geolocalização. Sem window-management. Sem idle-detector. Negue tudo por padrão.
  • RPC baseado em postMessage: MessageChannel isola comunicações em portas explícitas. Bibliotecas como Comlink tornam isso ergonômico sem vazar referências.

Os trade-offs são reais. Cada iframe custa memória (são comuns alguns MBs), complexidade de layout e uma camada de integração mais fina. Mas quando um plugin vem de “alguém na internet”, isso é a diferença entre um relatório de incidente e um post de autoelogio.

Um framework de decisão pragmático

1) Quem escreve o código e quem você vai responsabilizar quando der errado

  • Apenas times first-party: Use Web Components pela ergonomia e Shadow DOM para isolar estilos. Ainda assim, controle o acesso com APIs do host estreitas. Nada de estado global implícito.
  • Parceiros estratégicos validados, sob contrato: Comece com iframes para isolamento e, depois, exponha seletivamente superfícies “parecidas com DOM” via APIs do host. Se precisar usar Web Components, envolva o código do parceiro em um Worker e intermedeie todas as chamadas ao host via um MessageChannel.
  • Marketplace aberto: Iframes cross-origin com sandbox e Permissions Policy. Sem exceções.

2) O que o plugin precisa fazer

  • Widgets de UI somente leitura (tabelas, gráficos, painéis): Web Components servem para first-party. Para terceiros, renderize dentro de um iframe; passe snapshots de dados, não handles.
  • Fluxos que alteram estado (create/update/delete no seu app): Sempre delimite capacidades via tokens de ação assinados, emitidos no servidor por tenant-recurso-operação com TTL de 5–15 minutos. Nada de bearer tokens com autoridade global.
  • Cálculo pesado (parsing, transformações de IA): Desloque para um Worker dentro da origem do iframe ou para seu runtime de extensões no servidor. Evite long tasks na thread principal: reserve no máximo 50 ms por interação; faça throttle ou ceda com scheduler.postTask({priority: "background"}).
  • Acesso de rede: Prefira uma API de fetch proxyada pelo host que imponha CORS, registre requisições, remova cookies e aplique rate limits. Não conceda rede crua a plugins não confiáveis.

3) Quão profunda precisa ser a integração visual

  • “Com cara de parte do app”: Para iframes, entregue uma ponte de design tokens: exponha variáveis CSS e uma paleta claro/escuro via uma mensagem de handshake quando o iframe carregar. Não compartilhe todo o runtime do seu design system; envie tokens resolvidos.
  • Eventos, foco, ARIA: Para acessibilidade, faça proxy de eventos de nível superior do host (resize, theme-change, locale-change) por uma MessagePort dedicada. Dentro do iframe, o plugin é dono da semântica ARIA. Você não consegue garantir a11y entre frames com o mesmo rigor de árvores shadow; ajuste essa expectativa.
  • Drag/drop, seleção, overlays/portals: Esses pontos doem. Forneça primitivos gerenciados pelo host: API de overlay portal de propriedade do host; coordenação de drag/drop controlada pelo host que envia intents aos plugins em vez de conceder listeners globais.

Controles de segurança que você deve ligar desde o primeiro dia

  • CSP no host e nas origens de plugins: Host: script-src 'self' + hashes para seus scripts, nada de 'unsafe-inline'. Origem do plugin: CSP separado sem execução de código remoto além do bundle. Para cargas de Web Components, fixe com SRI e versões exatas.
  • Baseline de Permissions Policy: Negue tudo por padrão: geolocation=(), camera=(), microphone=(), payment=(), usb=(), battery=(), clipboard-write=() a menos que exista uma necessidade concreta de produto.
  • Contrato de sandbox do iframe: sandbox="allow-scripts allow-forms" no mínimo; evite allow-same-origin. Adicione allow-popups e allow-popups-to-escape-sandbox apenas para fluxos de OAuth; encerre os canais de mensagem após o término da autenticação.
  • Tokens de capacidade, não cookies de sessão: Emita JWTs estreitos: {tenant, resource, operation, plugin_id, exp ≤ 15m}. Vincule-os ao iframe via inicialização por message-port; nunca exponha tokens de sessão globais ao JS do plugin.
  • Rate limiting e logging de egress por plugin_id: Toda chamada via ponte do host é medida e marcada com identificadores de tenant e plugin. Você vai precisar disso quando o Financeiro perguntar por que um plugin explodiu o egress em 3 TB.
  • Trusted Types + DOMPurify (rigoroso): Para qualquer renderização de HTML que o host fizer em nome de um plugin, exija TrustedHTML. Trate HTML entre frames como hostil até ser sanitizado.

Orçamentos de performance e UX que você pode impor

  • Memória: Espere 3–8 MB de baseline por iframe no Chromium moderno. Em um dashboard com 6 plugins, você acabou de gastar ~30–50 MB. Faça disso um limite rígido: no máximo 4 iframes de terceiros simultâneos visíveis; lazy-load no restante.
  • Latência de interação (INP): Orçamento ≤ 200 ms p95. Qualquer plugin gerando long tasks > 50 ms mais de 5 vezes em um minuto é estrangulado via seu scheduler do host e sinalizado no marketplace.
  • Rede: Limite as requisições de saída do plugin a 10 RPS por tenant por padrão, com burst ≤ 50. Qualquer coisa relacionada a IA deve ir para seu runtime de extensões no servidor para manter o gasto de tokens e a latência previsíveis.
  • Cold start: Iframes são mais lentos para inicializar. Pré-inicialize iframes ocultos com loading="lazy" e um handshake explícito de aquecimento durante janelas ociosas. Espere 80–200 ms extras de TTI vs. código in-page, dependendo do tamanho do bundle.

Experiência do desenvolvedor que você deve ao seu ecossistema

Segurança sem boa DX morre no primeiro contato com a realidade. Dê aos desenvolvedores de plugins um contrato fácil de seguir e difícil de usar errado:

  • Manifesto + capacidades: Um manifest JSON declarando rotas, pontos de montagem de UI e capacidades solicitadas. Rejeite no registro se pedir mais do que é permitido para sua categoria.
  • Host API tipada: Entregue um SDK TypeScript que encapsule postMessage com Comlink, imponha escopos de capacidade em tempo de compilação e forneça mocks para dev local.
  • Design tokens, não seu CSS: Exporte tokens de cor, espaçamento e tipografia via um schema JSON estável; os plugins mapeiam seus próprios componentes. Não acople seu ritmo de releases ao CSS de terceiros.
  • Contêiner de dev local: Um CLI que sobe um host de iframe em localhost com o mesmo sandbox e políticas que você roda em produção. Nada de “funciona localmente” divergente.
  • Observabilidade: Forneça um console do plugin: logs, chamadas de rede, acertos de rate-limit, negações de capacidade. Exponha tempo p95 de init e INP para que desenvolvedores possam ajustar.

Política de marketplace que realmente transfere responsabilidade

Se você vai permitir que código externo rode contra os dados dos seus clientes, opere como uma plataforma:

  • Níveis de revisão de segurança: Bronze (checks automatizados apenas), Silver (revisão humana + varredura de código + teste de penetração), Gold (re-cert anual, relatório SOC 2). Precifique sua participação na receita de acordo.
  • Key escrow para takedowns: Cada release de plugin deve ser endereçável: bundle assinado com metadados de build reprodutível. Se você precisar revogar, consegue. Use Subresource Integrity para entrega de scripts quando viável.
  • Playbook de incidentes: Desabilitar com um clique por tenant e kill switch global. Exija que plugins implementem uma rota de “modo seguro” para exportar dados e desinstalar.
  • Pegada de dados clara: Documente publicamente o que um plugin pode fazer e quais capacidades usa. Torne pesquisável para que compradores filtrem por tolerância a risco.

Extensões server-side: não force tudo para o navegador

Muito do “precisamos de acesso profundo ao DOM” é autoinfligido. Separe o mundo:

  • Superfícies visuais client-side: Use iframes ou Web Components conforme as regras acima.
  • Ações e automações server-side: Rode no seu runtime de extensões (Node, Deno ou WASM) com limites estritos de tenancy, egress auditado e requisições assinadas de volta às suas APIs core. A UI do navegador apenas dispara intents.

Fazer isso reduz muito a pressão por superpermissões no plugin do navegador apenas para alcançar dados ou GPUs que ele nunca deveria tocar.

O que há de novo em 2026 que você deve observar

  • Descontinuação de cookies de terceiros: Joga a seu favor. Iframes cross-origin não herdarão sua sessão. Projete com capacidades baseadas em token desde o primeiro dia.
  • Fenced Frames: Ótimos para conteúdo não confiável tipo anúncio, com garantias de privacidade, mas ainda muito restritos para a maioria dos plugins de app. Acompanhe o suporte; não aposte a plataforma nisso ainda.
  • Base UI, shadcn/ui churn: As migrações recentes de bibliotecas de componentes lembram: design systems mudam; limites de segurança não. Mantenha segurança ortogonal à sua stack de UI.

Uma arquitetura de referência que você pode copiar

  1. Serviço de registro: Armazena manifests de plugins, metadados de bundles assinados e capacidades. Impõe políticas por categoria no momento da publicação.
  2. Shell do host (app): Renderiza pontos de montagem de plugins. Para plugins não confiáveis, injeta iframes cross-origin com sandbox e atributos allow por capacidade. Inicializa um MessageChannel por plugin.
  3. Host SDK: Wrapper com tipagem forte em cima de postMessage/Comlink. Expõe APIs: data.read(resource), actions.execute(actionToken), ui.openOverlay(), storage.session.get/set (com escopo para plugin_id+tenant).
  4. Policy gateway: Serviço no servidor que emite tokens de capacidade de curta duração e faz proxy do egress de rede de plugins com logging e rate limits. Vincula tokens a tenant+plugin_id+operation.
  5. Runtime de extensões server-side: Executa etapas pesadas ou sensíveis sob seu controle. Comunica-se com serviços core via mTLS e requisições assinadas. Expõe webhooks para o iframe do plugin receber atualizações.
  6. Plano de observabilidade: Dashboards por plugin: tempo de init, INP, taxa de erro, egress, negações de capacidade. Flags automáticas no marketplace para outliers.
  7. Kill switch e feature flags: Toggles centralizados para revogar uma versão ou capacidade instantaneamente em todos os tenants.

Armadilhas comuns (e a correção)

  • Armadilha: Compartilhar sua store Redux ou React context com um plugin em Web Component. Correção: Expor snapshots somente leitura via structured clone; todas as gravações passam por ações do host com escopo de capacidade.
  • Armadilha: Adicionar allow-same-origin ao iframe porque um dev precisava de cookies. Correção: Não faça isso. Faça proxy da autenticação via o host; se for imprescindível, isole esse fluxo em um iframe de auth dedicado com um canal em quarentena e destrua-o após o uso.
  • Armadilha: Permitir que plugins registrem listeners de eventos globais. Correção: Forneça APIs de eventos com escopo, de propriedade do host, e faça throttle nelas.
  • Armadilha: Deriva de versão do seu design system quebra plugins. Correção: Versione seus design tokens e suporte pelo menos dois majors concorrentes. O contrato do plugin são tokens, não nomes de classe.

Por que isso importa para apps pesados em IA

Integrações de agentes ampliam risco. Um plugin que pode ver tudo acabará, cedo ou tarde, fazendo prompt-injection para conseguir fazer de tudo. Mantenha contextos de modelo estreitos: quando um plugin pede ajuda de IA, o host monta fatias de contexto, remove segredos e assina a requisição para o modelo. Nenhum plugin deve jamais manter chaves de API brutas ou embeddings globais por tenant. Espere que reguladores se importem — em breve.

O que fazemos para clientes na DHD Tech

Já entregamos plataformas de plugins para equipes de SaaS nos EUA em que o delta entre “app bacana” e “plataforma defensável” é de semanas, não trimestres. Nossa stack padrão é:

  • Iframes cross-origin para código de terceiros com sandbox e Permissions Policy rigorosa.
  • APIs de host tipadas sobre Comlink, com JWTs por tenant e por capacidade expirando em 5–15 minutos.
  • Pontes de design tokens para coesão visual sem emaranhar CSS.
  • Runtimes de extensões no servidor para cargas pesadas/reguladas, com egress orçado e contabilização de tokens.
  • Testes automatizados de conformidade que sobem 50+ plugins sintéticos em CI para validar isolamento, rate limits e orçamentos de performance sob carga.

Isso é propositalmente sem glamour. A alternativa é acordar com um thread de violação alegando que “vídeos privados” de alguém vazaram por causa do seu bug de cross-tenant. Não seja esse post.

Principais lições

  • Use iframes cross-origin para qualquer código que você não controla; Web Components servem para composição, não para contenção.
  • Exponha APIs do host estreitas e com escopo de capacidades; nunca passe DOM, storage ou tokens globais brutos para plugins.
  • Faça cumprir orçamentos: memória por iframe, p95 de INP, RPS de rede e kill switches para maus atores.
  • Design tokens fazem a ponte da integração visual; não compartilhe o runtime do seu design system.
  • Separe UI no cliente de extensões no servidor; mantenha o que é sensível e pesado fora do navegador.
  • Invista em manifest, SDK TypeScript, host local e observabilidade para tornar o caminho seguro o caminho fácil.

Autor: Diogo Hudson Dias

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation