
Uma base de código, dois apps: como fazer white-label sem bifurcar
GeekBye e Pavleur são dois apps de desktop com marcas diferentes construídos a partir de um único repositório — sem fork, sem base de código duplicada. Esta é a maquinaria de build que faz uma única base de código compilar em dois produtos, e o bug de uma linha que fez o nosso segundo app apresentar-se com o nome errado.
GeekBye é um anotador de reuniões. Pavleur é um app de prática de entrevistas com IA. Parecem dois produtos diferentes porque são dois produtos diferentes — mas são construídos a partir de uma única base de código, sem fork e sem um branch que se desvia. Este é o lado de engenharia do white-label: não "por que vender um app de marca" (isso é outro artigo), mas como você de fato compila um repositório em dois apps — e o bug pouco glamouroso que se esconde em todo sistema white-label até você ir procurá-lo.
Gere, não ramifique
O jeito errado de rodar dois produtos a partir de uma base de código é espalhar if (product === 'pavleur') por todo o app, ou bifurcar e rezar para que as duas cópias nunca divirjam. Ambos apodrecem rápido.
A abordagem do GeekBye é a geração de código em tempo de build. Cada produto tem um arquivo de config — configs/geekbye/config.ts, configs/pavleur/config.ts — que guarda sua identidade: nome, bundle id, protocol de URL, o repositório de releases para as atualizações automáticas, e assim por diante. Antes de um build, um script roda:
node scripts/build-product.js pavleur
Esse script lê a config do produto escolhido e faz três coisas concretas:
- Gera dois módulos de config — um para o renderer (
src/config/product.ts), outro para o processo principal (electron/config/product.ts) — cada um carimbado com um cabeçalho "DO NOT EDIT MANUALLY — changes will be overwritten". O resto do app apenas importaproductName,applicationIdeprotocolda config e nunca pensa em qual marca ele é. - Copia apenas os assets daquela marca — ícones, o logo pequeno, o ícone de notificação toast. Um build do GeekBye não contém fisicamente nenhuma arte do Pavleur e vice-versa. Nada para vazar.
- Reescreve
package.json(em builds de release) — o app id, o nome de produto, o protocol de URL, o repositório de releases do GitHub onde publicar e, muito a propósito, o texto de permissão de microfone com a marca para que o macOS pergunte "Pavleur gostaria de acessar o microfone", não a marca errada.
O app inteiro é agnóstico à marca; a marca é injetada em tempo de build. Não há um interruptor de produto em runtime para errar, porque em runtime só existe um produto assado dentro.
Um backend, duas marcas
Ambos os apps falam com o mesmo backend. Então como o servidor sabe a qual produto — e a quais preços, prompts e limites — uma requisição pertence?
Dois sinais, ambos fixados em tempo de build. Primeiro, um application id (geekbye ou pavleur) assado na config e enviado com as requisições, para que o backend possa rotear para o comportamento do produto certo. Segundo, um User-Agent que o cliente anexa a cada chamada ao backend, na forma Product/version (platform) — p. ex. Pavleur/1.2.5 (win32) ou GeekBye/1.7.3 (darwin). Esse único cabeçalho dá ao backend atribuição por produto e por SO de graça, sem um campo de analytics separado. As atualizações automáticas, enquanto isso, ficam isoladas: cada produto publica no seu próprio repositório de releases, então uma atualização do GeekBye nunca pode ser servida a uma instalação do Pavleur.
O onboarding também difere por produto — o assistente do GeekBye é sobre transcrição de reuniões, o do Pavleur é sobre prática de entrevistas — e o certo é escolhido em runtime a partir do único application id assado dentro. A mesma maquinaria, duas portas de entrada.
O bug: quando o Pavleur se chamava GeekBye
Aqui está o causo de guerra, e é aquele de que todo sistema white-label tem uma versão.
A geração de config funcionava. Os ícones trocavam. O prompt de microfone tinha a marca. Os protocols e os bundle ids estavam certos. Por toda medida visível, um build do Pavleur era Pavleur. E mesmo assim, por um tempo, cada build do Pavleur ligava para casa no backend chamando-se GeekBye.
O culpado era uma única string escrita à mão, e ela se escondia no lugar menos glamouroso imaginável: o construtor do User-Agent, enfiado dentro de um helper de retry de rede. Tudo voltado ao usuário havia sido roteado pela config, mas esse utilitário de encanamento profundo ainda tinha um literal `GeekBye/${version}` dentro. Ninguém pensa num utilitário de retry como "branding", então ninguém o havia verificado. A correção foram quatro linhas — importar o productName dinâmico da config e interpolá-lo em vez da marca escrita à mão — mas a lição é o ponto inteiro:
White-label não é "o logo mudou". É "cada string de identidade passa pela config". E os últimos retardatários nunca vivem na UI, onde você olharia. Eles vivem no User-Agent, no manipulador de protocol de deep-link, no texto do prompt de permissão, nas mensagens de erro — o encanamento que ninguém arquiva sob "marca". A única defesa confiável é tratar o literal de marca como algo que você caça ativamente: dê grep na base de código pelo nome escrito à mão e faça o build falhar se ele aparecer em qualquer lugar fora da config que deveria defini-lo.
Três coisas que o white-label nos ensinou
- Gere, não ramifique. A geração em tempo de build de dois módulos de config minúsculos mais uma cópia seletiva de assets vence tanto o fork quanto um matagal de verificações de produto em runtime. O app permanece agnóstico à marca; a marca é uma entrada do build.
- Strings de identidade vazam para o encanamento. A UI é os 90% fáceis. Os 10% difíceis são o User-Agent, o protocol de URL, a string de permissão de microfone, o texto de erro. Roteie todos por um único export
productName/applicationId— e dê grep pelo nome de marca cru como portão de CI para que um literal perdido nunca possa ser lançado. - Um backend, muitas marcas, desambigue por id e cabeçalho. Um application id fixado em tempo de build mais um User-Agent
Product/version (platform)dá atribuição por produto e por plataforma sem infraestrutura duplicada — separando limpamente a identidade imutável de tempo de build do comportamento em runtime.
Para o lado de produto e privacidade de rodar um anotador white-label sob a sua própria marca, veja o anotador de IA white-label. Este é o sexto capítulo da história v1 que se torna GeekBye v2 — para o capítulo anterior, o que é realmente um lançamento de 127 commits (v1.7.0); e para todo o arco, a anatomia de entregar software até a perfeição.