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

Sortir le backend du chemin d'upload
Steven
Steven9 min de lecture

Sortir le backend du chemin d'upload

GeekBye enregistre votre écran et sauvegarde la vidéo sur votre Google Drive. La première version faisait passer chaque enregistrement par les propres serveurs de GeekBye en chemin ; une version plus tard, le fichier allait directement de votre machine à Drive, et le backend était rétrogradé à ne tenir qu'un simple pointeur. Le plus intéressant est le peu de code que contient réellement la version « directe et reprenable » — parce que la reprise venait de la suppression d'un proxy, pas de l'écriture d'un.

Ingénierie
Architecture
Desktop
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
Le silence était porteur
Steven
Steven9 min de lecture

Le silence était porteur

Les deux dernières versions de GeekBye v1 tournent autour de la même vérité inconfortable : la transcription en temps réel sur un vrai réseau n'est pas sans perte, et le geste honnête est de cesser de prétendre le contraire. La v1.8.20 gardait sur disque une copie de chaque chunk audio avant de le jeter pendant une reconnexion, et s'est mise à marquer les trous dans la transcription à voix haute. La v1.9.0 a cessé d'envoyer le silence pour économiser de la bande passante — et a découvert que le silence était justement le signal que le transcripteur utilisait pour savoir qu'une phrase s'était terminée. Deux versions sur le coût de jeter les choses.

Ingénierie
Audio
Fiabilité