
Una base de código, dos apps: cómo hacer white-label sin bifurcar
GeekBye y Pavleur son dos apps de escritorio con marcas distintas construidas desde un único repositorio — sin fork, sin base de código duplicada. Esta es la maquinaria de build que hace que una sola base de código compile en dos productos, y el bug de una línea que hizo que nuestra segunda app se presentara con el nombre equivocado.
GeekBye es un tomador de notas de reuniones. Pavleur es una app de práctica de entrevistas con IA. Parecen dos productos distintos porque lo son — pero se construyen desde una sola base de código, sin fork y sin una rama que se desvíe. Este es el lado de ingeniería del white-label: no "por qué vender una app de marca" (eso es otro artículo), sino cómo compilas de verdad un repositorio en dos apps — y el bug poco lucido que se esconde en todo sistema white-label hasta que vas a buscarlo.
Genera, no ramifiques
La forma equivocada de correr dos productos desde una base de código es salpicar if (product === 'pavleur') por toda la app, o bifurcar y rezar para que las dos copias nunca diverjan. Ambas se pudren rápido.
El enfoque de GeekBye es la generación de código en tiempo de build. Cada producto tiene un archivo de config — configs/geekbye/config.ts, configs/pavleur/config.ts — que guarda su identidad: nombre, bundle id, protocol de URL, el repositorio de releases para las auto-actualizaciones, y demás. Antes de un build, se ejecuta un script:
node scripts/build-product.js pavleur
Ese script lee la config del producto elegido y hace tres cosas concretas:
- Genera dos módulos de config — uno para el renderer (
src/config/product.ts), otro para el proceso principal (electron/config/product.ts) — cada uno estampado con una cabecera "DO NOT EDIT MANUALLY — changes will be overwritten". El resto de la app simplemente importaproductName,applicationIdyprotocoldesde config y nunca piensa en qué marca es. - Copia solo los assets de esa marca — iconos, el logo pequeño, el icono de notificación toast. Un build de GeekBye no contiene físicamente nada de arte de Pavleur y viceversa. Nada que filtrar.
- Reescribe
package.json(en builds de release) — el app id, el nombre de producto, el protocol de URL, el repositorio de releases de GitHub donde publicar y, muy oportunamente, el texto de permiso de micrófono con la marca para que macOS pregunte "Pavleur quiere acceder al micrófono", no la marca equivocada.
Toda la app es agnóstica a la marca; la marca se inyecta en tiempo de build. No hay un cambio de producto en runtime que puedas equivocar, porque en runtime solo hay un producto horneado.
Un backend, dos marcas
Ambas apps hablan con el mismo backend. Entonces, ¿cómo sabe el servidor a qué producto — y a qué precios, prompts y límites — pertenece una petición?
Dos señales, ambas fijadas en tiempo de build. Primero, un application id (geekbye o pavleur) horneado en la config y enviado con las peticiones, para que el backend pueda enrutar al comportamiento del producto correcto. Segundo, un User-Agent que el cliente adjunta a cada llamada al backend, con la forma Product/version (platform) — p. ej. Pavleur/1.2.5 (win32) o GeekBye/1.7.3 (darwin). Esa única cabecera le da al backend atribución por producto y por SO gratis, sin un campo de analítica aparte. Las auto-actualizaciones, mientras tanto, quedan aisladas: cada producto publica en su propio repositorio de releases, así que una actualización de GeekBye nunca puede servirse a una instalación de Pavleur.
El onboarding también difiere por producto — el asistente de GeekBye va de transcripción de reuniones, el de Pavleur de práctica de entrevistas — y el correcto se elige en runtime a partir del único application id horneado. La misma maquinaria, dos puertas de entrada.
El bug: cuando Pavleur se llamaba a sí mismo GeekBye
Aquí está la batallita, y es de la que todo sistema white-label tiene una versión.
La generación de config funcionaba. Los iconos se cambiaban. El prompt de micrófono llevaba la marca. Los protocols y los bundle ids eran correctos. Por toda medida visible, un build de Pavleur era Pavleur. Y sin embargo, durante un tiempo, cada build de Pavleur llamaba a casa al backend presentándose como GeekBye.
El culpable era una única cadena escrita a mano, y se escondía en el lugar menos lucido imaginable: el constructor del User-Agent, metido dentro de un helper de reintentos de red. Todo lo de cara al usuario se había enrutado por config, pero esta utilidad de fontanería profunda seguía teniendo un literal `GeekBye/${version}` dentro. Nadie piensa en una utilidad de reintentos como "branding", así que nadie la había revisado. El arreglo fueron cuatro líneas — importar el productName dinámico desde config e interpolarlo en vez de la marca escrita a mano — pero la lección es todo el asunto:
El white-label no es "¿cambió el logo?". Es "¿pasa cada cadena de identidad por la config?". Y los últimos rezagados nunca viven en la UI, donde mirarías. Viven en el User-Agent, el manejador de protocol de deep-link, el texto del prompt de permisos, los mensajes de error — la fontanería que nadie archiva bajo "marca". La única defensa fiable es tratar el literal de marca como algo que cazas activamente: haz grep en la base de código por el nombre escrito a mano y falla el build si aparece en cualquier sitio fuera de la config que se supone que lo define.
Tres cosas que nos enseñó el white-label
- Genera, no ramifiques. La generación en tiempo de build de dos módulos de config diminutos más una copia selectiva de assets le gana tanto al fork como a una espesura de comprobaciones de producto en runtime. La app se mantiene agnóstica a la marca; la marca es una entrada del build.
- Las cadenas de identidad se filtran a la fontanería. La UI es el 90% fácil. El 10% difícil es el User-Agent, el protocol de URL, la cadena de permiso de micrófono, el texto de error. Enrútalos todos por un único export
productName/applicationId— y haz grep del nombre de marca crudo como puerta de CI para que un literal despistado nunca pueda lanzarse. - Un backend, muchas marcas, desambigua por id y cabecera. Un application id fijado en tiempo de build más un User-Agent
Product/version (platform)da atribución por producto y por plataforma sin infraestructura duplicada — separando limpiamente la identidad inmutable de tiempo de build del comportamiento en runtime.
Para el lado de producto y privacidad de correr un tomador de notas white-label bajo tu propia marca, mira el tomador de notas con IA white-label. Este es el sexto capítulo de la historia v1 que se convierte en GeekBye v2 — para el capítulo anterior, qué es realmente un lanzamiento de 127 commits (v1.7.0); y para todo el arco, la anatomía de publicar software hasta la perfección.