Como abri meu launcher para plugins de terceiros (e por que foi mais difícil do que parecia)
No post anterior eu expliquei como construí a Bely — um launcher para Windows com 70+ ferramentas usando Tauri 2 e React. Agora quero contar sobre uma decisão que tomei essa semana: abrir o app para plugins externos.
Por que plugins
A Bely já tem muita coisa built-in. Mas eu sei que em algum momento vou precisar que outras pessoas consigam estender o app sem depender de mim pra cada feature nova. Integração com API interna de empresa, lookup de pacote npm, ferramentas de nicho — são coisas que fazem sentido pra alguém mas não pra todo mundo.
Ao invés de esperar essa demanda aparecer, decidi construir a infraestrutura agora enquanto o projeto ainda é pequeno e eu consigo iterar rápido. Se der errado, pelo menos aprendi. Se der certo, o app já está pronto pra escalar.
O problema do iframe sandbox
A primeira decisão foi: como isolar o código de terceiros? Eu não posso simplesmente executar JS arbitrário no contexto do meu app — isso daria acesso ao filesystem, clipboard, e todas as APIs do Tauri.
A solução foi usar iframes com sandbox="allow-scripts". O plugin roda num documento HTML separado, carregado via blob URL, sem acesso ao DOM pai. Toda comunicação passa por postMessage.
Parece simples, mas tem uma cascata de problemas:
1. O iframe não herda o tema. O plugin renderiza num contexto CSS completamente isolado. Tive que coletar todas as CSS variables do tema glass (--glass-bg, --glass-text, --accent, etc — são mais de 40) e injetar como inline style no HTML do iframe. Se o usuário troca o tema, preciso mandar uma mensagem pro iframe atualizar.
2. O iframe rouba o foco da janela. Esse foi o bug mais irritante. No Windows, quando o acrylic blur está ativo e o usuário clica dentro do iframe (num input de busca do plugin, por exemplo), o window do documento pai dispara um evento blur. O app interpretava isso como "janela perdeu foco" e trocava o background de translúcido para opaco — matando o efeito glass.
A correção foi trocar o listener de window.blur/focus do DOM pela API nativa do Tauri (getCurrentWindow().onFocusChanged()), que detecta foco no nível do sistema operacional. O iframe está na mesma janela do OS, então o foco continua "ativo" mesmo quando o usuário interage com o plugin.
3. CORS mata tudo. Os plugins precisam fazer fetch pra APIs externas (npm registry, GitHub, etc). Mas o fetch roda no contexto do host app que está em http://tauri.localhost. A maioria das APIs bloqueia por CORS.
A solução foi criar um proxy no Rust. O plugin manda uma mensagem { type: "api-call", method: "fetch", args: [url, options] }, o host repassa pro backend Rust que faz o request via reqwest (sem restrição de CORS) e devolve o resultado.
O SDK
Não dá pra esperar que alguém escreva HTML cru com postMessage. Criei um SDK (@usebely/sdk) que abstrai tudo:
import { mount, List, useFetch } from "@usebely/sdk";
function App() {
const [query, setQuery] = useState("");
const { data, loading } = useFetch(
query.length >= 2
? `https://registry.npmjs.org/-/v1/search?text=${query}`
: null
);
return (
<List searchBarPlaceholder="Buscar pacotes..." onSearchChange={setQuery} isLoading={loading}>
{data?.objects?.map((obj) => (
<List.Item
key={obj.package.name}
title={obj.package.name}
subtitle={obj.package.description}
/>
))}
</List>
);
}
mount(<App />);
O SDK fornece componentes que já seguem o design system glass da Bely: List, Detail, Form, Grid, Action, ActionPanel. Todos usam as CSS variables injetadas, então o plugin automaticamente respeita o tema do usuário.
O useFetch tem debounce de 300ms e roteia pelo proxy Rust. O mount() cuida de montar o React, receber mensagens do host, e notificar quando o plugin está pronto.
Permissões
Cada plugin declara no manifest quais APIs precisa:
{
"permissions": ["fetch", "clipboard.write"]
}
Se o plugin tenta chamar algo que não declarou, o host bloqueia. As permissões disponíveis são: fetch, clipboard.read, clipboard.write, fs.read, fs.write, system.info. O usuário vê as permissões antes de instalar.
A Plugin Store
O fluxo:
- Dev builda o plugin e abre Plugin Store > Enviar na Bely
- Preenche metadados (nome, ícone, categoria) e seleciona a pasta do plugin
- O app cria um
.tar.gzcom o código completo (incluindo source pra review) e faz upload pro S3 - Plugin fica pendente até ser aprovado no painel admin
- Quando aprovado, aparece na store pra instalar
Versionamento: versão aprovada não pode ser editada — precisa submeter uma nova. Versões pendentes podem ser editadas antes da aprovação. Bundle tem limite de 5MB.
No app, quando um plugin instalado tem versão nova na store, o card mostra "Atualizar" em vez de "Instalado". O detail view mostra o changelog de cada versão.
O que aprendi
Isolamento é caro. O iframe sandbox resolve segurança mas cria uma barreira de comunicação. Cada feature que funciona "de graça" no app principal (tema, foco, fetch, clipboard) precisa de uma ponte explícita pro plugin.
O SDK é o produto. Se for difícil criar um plugin, ninguém vai criar. Investir tempo nos componentes e na DX valeu mais do que qualquer feature da store.
Review manual é o certo por enquanto. Hoje eu baixo o bundle, leio o source, testo. Não escala, mas com zero plugins de terceiros no momento, é melhor garantir qualidade do que automatizar algo que ninguém está usando.
Glass theme em iframes é sofrimento. A quantidade de edge cases entre transparência, composição de rgba, e comportamento diferente entre Windows 10 e 11 é absurda. Se eu fosse começar de novo, talvez considerasse web components no mesmo documento ao invés de iframes.
Status
O sistema de plugins está na v1.26.0. O SDK é funcional e já tem um plugin de exemplo (NPM Lookup) na store. A documentação está em https://bely.my/docs/plugins
A Bely está em whitelist beta: https://bely.my
Feedback técnico é bem-vindo — especialmente de quem já lidou com plugin systems ou iframe sandboxing.
Fonte: https://bely.my