Steven
Steven6 min de lecture

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.

Ingénierie
Architecture
Compilation
Versions de GeekBye
Une base de code, deux apps : le white-label sans forker

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 :

  1. 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''importer productName, applicationId et protocol depuis config et ne pense jamais à quelle marque il est.
  2. 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.
  3. 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

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

Articles Connexes

On a supprimé 5,000 lignes de code audio — et les transcriptions ont commencé à s'afficher en double
Steven
Steven7 min de lecture

On a supprimé 5,000 lignes de code audio — et les transcriptions ont commencé à s'afficher en double

GeekBye v1.6 a arraché deux transcripteurs Swift embarqués et les a remplacés par un pipeline unifié — une suppression nette de plus de 5,000 lignes, réalisée pendant que des utilisateurs étaient en pleine réunion. Puis les transcriptions ont commencé à apparaître en double, car le bug s'était déplacé vers la seule couche qui croyait encore qu'il y avait deux moteurs.

Ingénierie
Transcription
Architecture
La vérification de sécurité qui a rendu notre app impossible à quitter
Steven
Steven7 min de lecture

La vérification de sécurité qui a rendu notre app impossible à quitter

La mise à jour automatique a été la fonctionnalité la plus difficile que nous ayons jamais livrée — six versions en quatre jours pour l'empêcher de rendre l'app inutilisable. Le pire bug, c'est nous qui l'avons introduit en voulant être prudents : une vérification « de sécurité » de 500 millisecondes qui transformait une mise à jour ratée en un processus qu'on ne pouvait littéralement pas quitter.

Ingénierie
Electron
Fiabilité
Ce qu'est vraiment une release de 127 commits
Steven
Steven7 min de lecture

Ce qu'est vraiment une release de 127 commits

GeekBye v1.7.0, c'était 127 commits en onze jours. Vu de l'extérieur, on dirait cent petites choses. Vu de l'intérieur, c'étaient deux grosses fonctionnalités tressées ensemble — et l'une d'elles a été construite au mauvais endroit, puis arrachée et refaite en pleine release. Voici l'anatomie d'une grosse release.

Ingénierie
Release
Architecture