Projetos
Três coisas que construí, com o problema que motivou cada uma, a decisão que tomei e o que eu faria diferente hoje.
Bananablet
MV3 · Cloudflare Workers · R2 · Supabase · VPS
O Bananablet é uma extensão de navegador que adiciona funcionalidade a um cliente de jogo online. Comecei porque o jogo não tem API, não tem SDK e não oferece nenhum ponto de extensão oficial: quem quiser construir algo em cima dele precisa interceptar o WebSocket binário que o cliente usa para falar com o servidor. Está no ar desde novembro de 2025, com mais de 4.000 instalações e 1.9k pessoas usando por mês.
É projeto meu, feito sozinho, e o único dos três que não saiu de um pedido de cliente.
Tecnologias
- Extensão em Manifest V3, injetada no contexto da página para alcançar o runtime do jogo
- Cloudflare Workers para a API de borda
- Cloudflare R2 para os assets e os áudios do chat
- Supabase, com Postgres, para o estado que precisa persistir
- Uma VPS em Node para a conexão WebSocket persistente
- Modelos de linguagem para tradução automática e moderação de conteúdo
- Suíte de testes com 197 arquivos
O que deu trabalho
Uma atualização do jogo passou a sortear os identificadores de pacote a cada conexão, derivando cada um de um hash sobre um desafio que o servidor manda no bootstrap. Isso quebrou 115 pontos do código de uma vez. Dava para remapear os valores em toda release, mas escolhi replicar a função de derivação do próprio cliente: refatorei 124 pontos e passei a cobrir cerca de 147 identificadores sem precisar de manutenção. O preço é ficar acoplado a um detalhe interno do jogo, e se eles trocarem o algoritmo eu descubro de novo do zero.
Medindo o Worker, encontrei 707 mil requisições em 24 horas, e quase tudo vinha da rota mais barata que eu tinha: cada cliente perguntando de tempos em tempos quem estava online. Migrei presença para uma conexão WebSocket persistente, e o tráfego caiu cerca de 95%.
Devia ter instrumentado antes. Só descobri isso numa investigação pontual, uma semana depois de o sistema já estar no ar, e com telemetria por rota desde o começo eu teria visto sozinho.
Try-On Virtual — Ótica Qualidade Visual
Node · TensorFlow.js · Gemini · Docker · Traefik
A Qualidade Visual é uma ótica aqui de Juazeiro do Norte, e o que eles queriam era simples de descrever e chato de resolver: o cliente vendo o óculos no próprio rosto antes de comprar, na página do produto, sem instalar nada e sem sair do fluxo de compra.
O problema é que a chamada ao Gemini que gera a imagem custa caro, demora, e às vezes volta com o rosto errado ou a armação torta. O caminho óbvio seria gerar, olhar e tentar de novo quando não presta, mas aí a espera multiplica pelo número de tentativas.
Tecnologias
- Node com Express, em dois serviços separados atrás de um proxy reverso
- TensorFlow.js para validação facial no servidor
- API de geração de imagem do Google Gemini para compor o óculos sobre a foto
- Postgres gerenciado para o estado do atendimento
- Docker e Traefik para o deploy na VPS
- Plugin WordPress próprio para embutir o widget na loja
- Detecção de pontos faciais no navegador, antes do upload
O que deu trabalho
Em vez de tentar em sequência, disparo várias gerações ao mesmo tempo e valido todas em paralelo, comparando cada candidata com a foto original por um embedding facial. Vence a de menor distância. Pago sempre por todas, inclusive quando a primeira já estava boa, mas a espera do usuário para de crescer com o número de tentativas.
Navegar o catálogo dentro do widget significava consultar a API da loja a cada interação, então construí três camadas de cache em memória, mais cache de imagem em disco. Medido em produção, o conjunto acerta 80,5% sobre 9.994 consultas.
Nessas mesmas medições apareceu algo que o desenho não previa: uma das três camadas nunca é acionada, porque a de busca sempre responde antes dela. Duas dão conta do mesmo trabalho. Foi medindo que eu descobri isso, não lendo o código, e é o tipo de coisa que só aparece com número na frente.
Ver a loja da Qualidade Visual
Feeds imobiliários — Alta Performance
PHP · WordPress · MySQL · XML
A Alta Performance é uma imobiliária que já tinha o acervo dentro do WordPress e precisava publicá-lo no Chaves na Mão, no Imovelweb e no VRSync, que é o feed do grupo OLX e alimenta o Vivareal e o ZAP. Cada um exige um arquivo XML no formato dele.
Gerar XML é a parte fácil. O difícil é que os três descrevem o mesmo imóvel de formas incompatíveis, e um imóvel válido para um volta rejeitado do outro com um erro genérico que nunca diz qual campo está errado.
Tecnologias
- PHP dentro do WordPress, sem framework, em 28 classes
- MySQL, com uma tabela própria para o log de validação por portal
- Geração de XML em streaming, para não montar o documento inteiro em memória
- Base local de 1.481 cidades, para resolver localidade sem chamada externa
- Cron do WordPress para regenerar os feeds
- Docker para o ambiente de desenvolvimento
O que deu trabalho
Os três falam vocabulários diferentes para a mesma coisa. O Chaves na Mão usa termos em português e exige 48 tags escritas sempre, mesmo vazias. O Imovelweb, cuja API é a OpenNavent, manda tudo em CDATA, com a data em epoch de milissegundos e a operação de preço em espanhol. O VRSync é o único com namespace e XSD, usa vocabulário em inglês, guarda a semântica nos atributos em vez dos elementos, e quer o CEP só com dígitos, ao contrário do Imovelweb. Uma classe abstrata cuida do que é comum e cada portal implementa só a montagem do próprio documento, com um validador de campos obrigatórios por portal. Assim o log diz exatamente o que faltou em cada um, e "o portal rejeitou" vira uma lista acionável.
A importação de anúncios de parceiros trazia 20 a 30 fotos por imóvel, o que estoura qualquer limite de execução num WordPress compartilhado. A saída comum é aumentar os limites, e não há uma única chamada dessas no plugin: o feed é baixado uma vez e fatiado em janelas de 50 itens, e o navegador encadeia as requisições, então cada processo PHP fica curto.
Os anúncios chegavam ao portal com a descrição virando uma parede de texto, embora estivesse correta no WordPress. Capturei o feed real que estava saindo e reproduzi a função de normalização fora do WordPress, o que transformou "o portal exibe errado" num teste de trinta linhas. Se eu tivesse escrito teste de fixture nos geradores desde o começo, teria pego esse e mais um antes do portal pegar.