Steven
Steven6 min de lectura

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.

Ingeniería
Arquitectura
Compilación
Lanzamientos de GeekBye
Una base de código, dos apps: cómo hacer white-label sin bifurcar

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:

  1. 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 importa productName, applicationId y protocol desde config y nunca piensa en qué marca es.
  2. 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.
  3. 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

  1. 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.
  2. 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.
  3. 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.

Artículos Relacionados

Una variable CSS, cinco rondas de revisión y una cadena de herramientas de Swift que mintió
Steven
Steven8 min de lectura

Una variable CSS, cinco rondas de revisión y una cadena de herramientas de Swift que mintió

La v2.0.7 de GeekBye hizo todo el overlay translúcido de forma ajustable — lo que suena a un cambio de CSS de una línea y desde luego no lo fue. La historia de verdad es lo que cazó la revisión de código: dos superficies que se negaban a difuminarse, una barra de grabación que se veía mal con la misma opacidad y un binario compilado que oscilaba 672 bytes porque nuestra propia documentación le dijo a un revisor la versión de Swift equivocada.

Ingeniería
Diseño
Compilación
Borramos 5,000 líneas de código de audio — y las transcripciones empezaron a aparecer dos veces
Steven
Steven7 min de lectura

Borramos 5,000 líneas de código de audio — y las transcripciones empezaron a aparecer dos veces

GeekBye v1.6 arrancó dos transcriptores Swift en local y los reemplazó con un único pipeline unificado — una eliminación neta de más de 5,000 líneas, hecha mientras los usuarios estaban en mitad de una reunión. Entonces las transcripciones empezaron a aparecer dos veces, porque el bug se trasladó a la única capa que seguía creyendo que había dos motores.

Ingeniería
Transcripción
Arquitectura
Qué es realmente un lanzamiento de 127 commits
Steven
Steven6 min de lectura

Qué es realmente un lanzamiento de 127 commits

GeekBye v1.7.0 fueron 127 commits en once días. Desde fuera parecen cien cositas. Desde dentro eran dos grandes funciones entretejidas — y una de ellas se construyó en el lugar equivocado, para luego arrancarla y rehacerla a mitad del lanzamiento. Esta es la anatomía de un lanzamiento grande.

Ingeniería
Lanzamiento
Arquitectura