
Une base de code, deux apps : le white-label sans forker
GeekBye et Pavleur sont deux apps de bureau aux marques différentes construites depuis un seul dépôt — sans fork, sans base de code dupliquée. Voici la mécanique de build qui fait compiler une seule base de code en deux produits, et le bug d'une ligne qui a fait que notre deuxième app s'est présentée sous le mauvais nom.
GeekBye est un preneur de notes de réunion. Pavleur est une app d''entraînement aux entretiens avec IA. Ils ressemblent à deux produits différents parce qu''ils le sont — mais ils sont construits depuis une seule base de code, sans fork et sans branche qui dérive. Voici le côté ingénierie du white-label : pas « pourquoi vendre une app de marque » (ça, c''est un autre article), mais comment on compile vraiment un dépôt en deux apps — et le bug peu glorieux qui se cache dans tout système white-label jusqu''à ce qu''on aille le chercher.
Générer, pas ramifier
La mauvaise façon de faire tourner deux produits depuis une base de code est de parsemer if (product === 'pavleur') dans toute l''app, ou de forker et de prier pour que les deux copies ne divergent jamais. Les deux pourrissent vite.
L''approche de GeekBye est la génération de code au moment du build. Chaque produit a un fichier de config — configs/geekbye/config.ts, configs/pavleur/config.ts — qui contient son identité : nom, bundle id, protocol d''URL, le dépôt de releases pour les mises à jour automatiques, et ainsi de suite. Avant un build, un script s''exécute :
node scripts/build-product.js pavleur
Ce script lit la config du produit choisi et fait trois choses concrètes :
- Génère deux modules de config — un pour le renderer (
src/config/product.ts), un pour le processus principal (electron/config/product.ts) — chacun estampillé d''un en-tête "DO NOT EDIT MANUALLY — changes will be overwritten". Le reste de l''app se contente d''importerproductName,applicationIdetprotocoldepuis config et ne pense jamais à quelle marque il est. - Copie uniquement les assets de cette marque — icônes, le petit logo, l''icône de notification toast. Un build de GeekBye ne contient physiquement aucun visuel de Pavleur et vice-versa. Rien à faire fuiter.
- Réécrit
package.json(sur les builds de release) — l''app id, le nom de produit, le protocol d''URL, le dépôt de releases GitHub où publier et, fort à propos, le texte d''autorisation micro à la marque pour que macOS demande « Pavleur souhaite accéder au microphone », pas la mauvaise marque.
Toute l''app est agnostique à la marque ; la marque est injectée au moment du build. Il n''y a pas de bascule de produit au runtime à rater, parce qu''au runtime il n''y a jamais qu''un seul produit cuit dedans.
Un backend, deux marques
Les deux apps parlent au même backend. Alors comment le serveur sait-il à quel produit — et à quels prix, prompts et limites — appartient une requête ?
Deux signaux, tous deux fixés au moment du build. D''abord, un application id (geekbye ou pavleur) cuit dans la config et envoyé avec les requêtes, pour que le backend puisse router vers le comportement du bon produit. Ensuite, un User-Agent que le client attache à chaque appel backend, de la forme Product/version (platform) — p. ex. Pavleur/1.2.5 (win32) ou GeekBye/1.7.3 (darwin). Cet unique en-tête donne au backend une attribution par produit et par OS gratuitement, sans champ d''analytics séparé. Les mises à jour automatiques, pendant ce temps, restent isolées : chaque produit publie dans son propre dépôt de releases, si bien qu''une mise à jour de GeekBye ne peut jamais être servie à une installation de Pavleur.
L''onboarding diffère aussi par produit — l''assistant de GeekBye parle de transcription de réunions, celui de Pavleur d''entraînement aux entretiens — et le bon est choisi au runtime à partir de l''unique application id cuit dedans. La même mécanique, deux portes d''entrée.
Le bug : quand Pavleur s''appelait GeekBye
Voici l''anecdote de guerre, et c''est celle dont tout système white-label a une version.
La génération de config marchait. Les icônes basculaient. Le prompt micro portait la marque. Les protocols et les bundle ids étaient corrects. Par toute mesure visible, un build de Pavleur était Pavleur. Et pourtant, pendant un temps, chaque build de Pavleur appelait chez lui le backend en se présentant comme GeekBye.
Le coupable était une seule chaîne écrite en dur, et elle se cachait à l''endroit le moins glorieux imaginable : le constructeur du User-Agent, niché dans un helper de retry réseau. Tout ce qui était visible par l''utilisateur avait été routé par config, mais cet utilitaire de plomberie profonde avait encore un littéral `GeekBye/${version}` dedans. Personne ne pense à un utilitaire de retry comme à du « branding », donc personne ne l''avait vérifié. Le correctif faisait quatre lignes — importer le productName dynamique depuis config et l''interpoler au lieu de la marque écrite en dur — mais la leçon, c''est tout le propos :
Le white-label, ce n''est pas « le logo a-t-il changé ». C''est « chaque chaîne d''identité passe-t-elle par la config ». Et les derniers récalcitrants ne vivent jamais dans l''UI, où l''on regarderait. Ils vivent dans le User-Agent, le gestionnaire de protocol de deep-link, le texte du prompt d''autorisation, les messages d''erreur — la plomberie que personne ne classe sous « marque ». La seule défense fiable est de traiter le littéral de marque comme une chose que l''on traque activement : faites un grep de la base de code sur le nom écrit en dur et faites échouer le build s''il apparaît où que ce soit hors de la config qui est censée le définir.
Trois choses que le white-label nous a apprises
- Générer, pas ramifier. La génération au moment du build de deux modules de config minuscules plus une copie sélective d''assets bat à la fois le fork et un fouillis de vérifications de produit au runtime. L''app reste agnostique à la marque ; la marque est une entrée du build.
- Les chaînes d''identité fuient dans la plomberie. L''UI, c''est les 90 % faciles. Les 10 % difficiles sont le User-Agent, le protocol d''URL, la chaîne d''autorisation micro, le texte d''erreur. Routez-les tous par un seul export
productName/applicationId— et faites un grep du nom de marque brut comme barrière de CI pour qu''un littéral égaré ne puisse jamais être livré. - Un backend, plusieurs marques, désambiguïser par id et en-tête. Un application id fixé au moment du build plus un User-Agent
Product/version (platform)donne une attribution par produit et par plateforme sans infrastructure dupliquée — séparant proprement l''identité immuable du moment du build du comportement au runtime.
Pour le côté produit et confidentialité de faire tourner un preneur de notes white-label sous votre propre marque, voir le preneur de notes IA white-label. Ceci est le sixième chapitre de l''histoire v1 qui devient GeekBye v2 — pour le chapitre précédent, ce qu''est vraiment une release de 127 commits (v1.7.0) ; et pour tout l''arc, l''anatomie de livrer un logiciel jusqu''à la perfection.