
Eine Codebasis, zwei Apps: White-Label ohne Fork
GeekBye und Pavleur sind zwei unterschiedlich gebrandete Desktop-Apps, gebaut aus einem einzigen Repository — kein Fork, keine duplizierte Codebasis. Hier ist die Build-Maschinerie, die eine Codebasis in zwei Produkte kompiliert, und der Ein-Zeilen-Bug, der unsere zweite App sich mit dem falschen Namen vorstellen ließ.
GeekBye ist ein Meeting-Notizen-Tool. Pavleur ist eine App zum Üben von Bewerbungsgesprächen mit KI. Sie wirken wie zwei verschiedene Produkte, weil sie es sind — aber sie werden aus einer Codebasis gebaut, ohne Fork und ohne einen Branch, der abdriftet. Das ist die Ingenieursseite von White-Label: nicht „warum eine gebrandete App verkaufen" (das ist ein anderer Artikel), sondern wie man ein Repository tatsächlich in zwei Apps kompiliert — und der glanzlose Bug, der sich in jedem White-Label-System versteckt, bis man ihn suchen geht.
Generieren, nicht verzweigen
Der falsche Weg, zwei Produkte aus einer Codebasis zu betreiben, ist, if (product === 'pavleur') über die ganze App zu streuen, oder zu forken und zu beten, dass die zwei Kopien nie auseinanderlaufen. Beides verrottet schnell.
GeekByes Ansatz ist die Code-Generierung zur Build-Zeit. Jedes Produkt hat eine Config-Datei — configs/geekbye/config.ts, configs/pavleur/config.ts — die seine Identität enthält: Name, bundle id, URL-protocol, das Releases-Repository für Auto-Updates und so weiter. Vor einem Build läuft ein Skript:
node scripts/build-product.js pavleur
Dieses Skript liest die Config des gewählten Produkts und tut drei konkrete Dinge:
- Generiert zwei Config-Module — eines für den Renderer (
src/config/product.ts), eines für den Hauptprozess (electron/config/product.ts) — jedes gestempelt mit einem "DO NOT EDIT MANUALLY — changes will be overwritten"-Header. Der Rest der App importiert einfachproductName,applicationIdundprotocolaus der Config und denkt nie darüber nach, welche Marke er ist. - Kopiert nur die Assets dieser Marke — Icons, das kleine Logo, das Toast-Benachrichtigungs-Icon. Ein GeekBye-Build enthält physisch keinerlei Pavleur-Grafik und umgekehrt. Nichts, was auslaufen könnte.
- Schreibt
package.jsonum (bei Release-Builds) — die app id, den Produktnamen, das URL-protocol, das GitHub-Releases-Repo zum Veröffentlichen und, sehr passend, den Mikrofon-Berechtigungstext mit der Marke, damit macOS fragt „Pavleur möchte auf das Mikrofon zugreifen", nicht die falsche Marke.
Die ganze App ist markenagnostisch; die Marke wird zur Build-Zeit injiziert. Es gibt keinen Produkt-Umschalter zur Laufzeit, den man falsch machen könnte, denn zur Laufzeit ist immer nur ein Produkt eingebacken.
Ein Backend, zwei Marken
Beide Apps sprechen mit dem selben Backend. Wie weiß der Server also, zu welchem Produkt — und zu welchen Preisen, Prompts und Limits — eine Anfrage gehört?
Zwei Signale, beide zur Build-Zeit fixiert. Erstens eine application id (geekbye oder pavleur), in die Config eingebacken und mit Anfragen gesendet, damit das Backend zum Verhalten des richtigen Produkts routen kann. Zweitens ein User-Agent, den der Client an jeden Backend-Aufruf anhängt, in der Form Product/version (platform) — z. B. Pavleur/1.2.5 (win32) oder GeekBye/1.7.3 (darwin). Dieser eine Header gibt dem Backend Zuordnung pro Produkt und pro OS gratis, ohne ein separates Analytics-Feld. Die Auto-Updates bleiben derweil isoliert: jedes Produkt veröffentlicht in sein eigenes Releases-Repository, sodass ein GeekBye-Update niemals an eine Pavleur-Installation ausgeliefert werden kann.
Das Onboarding unterscheidet sich ebenfalls pro Produkt — GeekByes Assistent geht um Meeting-Transkription, Pavleurs um das Üben von Bewerbungsgesprächen — und der richtige wird zur Laufzeit aus der einzigen eingebackenen application id gewählt. Dieselbe Maschinerie, zwei Eingangstüren.
Der Bug: als Pavleur sich GeekBye nannte
Hier ist die Kriegsgeschichte, und es ist die, von der jedes White-Label-System eine Version hat.
Die Config-Generierung funktionierte. Die Icons wechselten. Der Mikrofon-Prompt trug die Marke. Die protocols und die bundle ids stimmten. Nach jedem sichtbaren Maßstab war ein Pavleur-Build Pavleur. Und doch rief für eine Weile jeder Pavleur-Build beim Backend zu Hause an und nannte sich GeekBye.
Der Übeltäter war ein einziger fest verdrahteter String, und er versteckte sich am unglanzvollsten Ort, den man sich vorstellen kann: dem User-Agent-Builder, eingebettet in einen Netzwerk-Retry-Helper. Alles Nutzerseitige war durch die Config geroutet worden, aber dieses Utility der tiefen Klempnerei hatte immer noch ein Literal `GeekBye/${version}` darin. Niemand denkt bei einem Retry-Utility an „Branding", also hatte es niemand überprüft. Der Fix waren vier Zeilen — den dynamischen productName aus der Config importieren und ihn statt der fest verdrahteten Marke interpolieren — aber die Lektion ist der ganze Punkt:
White-Label ist nicht „hat sich das Logo geändert". Es ist „läuft jeder Identitäts-String durch die Config". Und die letzten Nachzügler leben nie in der UI, wo man nachschauen würde. Sie leben im User-Agent, im Deep-Link-protocol-Handler, im Text des Berechtigungs-Prompts, in den Fehlermeldungen — der Klempnerei, die niemand unter „Marke" ablegt. Die einzige verlässliche Verteidigung ist, das Marken-Literal als etwas zu behandeln, das man aktiv jagt: grep die Codebasis nach dem fest verdrahteten Namen und lass den Build fehlschlagen, wenn er irgendwo außerhalb der Config auftaucht, die ihn definieren soll.
Drei Dinge, die White-Label uns lehrte
- Generieren, nicht verzweigen. Die Build-Zeit-Codegen von zwei winzigen Config-Modulen plus selektivem Asset-Kopieren schlägt sowohl den Fork als auch ein Dickicht aus Produkt-Prüfungen zur Laufzeit. Die App bleibt markenagnostisch; die Marke ist ein Build-Input.
- Identitäts-Strings sickern in die Klempnerei. Die UI sind die leichten 90 %. Die schweren 10 % sind der User-Agent, das URL-protocol, der Mikrofon-Berechtigungs-String, der Fehlertext. Route sie alle durch einen einzigen
productName/applicationId-Export — und grep nach dem rohen Markennamen als CI-Gate, damit ein verirrtes Literal niemals ausgeliefert werden kann. - Ein Backend, viele Marken, unterscheiden per id und Header. Eine zur Build-Zeit fixierte application id plus ein
Product/version (platform)User-Agent gibt Zuordnung pro Produkt und pro Plattform ohne duplizierte Infrastruktur — sauber die unveränderliche Build-Zeit-Identität vom Laufzeitverhalten trennend.
Für die Produkt- und Datenschutzseite des Betriebs eines White-Label-Notiztools unter der eigenen Marke siehe das White-Label-KI-Notiztool. Dies ist das sechste Kapitel der v1-Geschichte, die zu GeekBye v2 wird — für das vorige Kapitel, was ein 127-Commit-Release wirklich ist (v1.7.0); und für den ganzen Bogen, die Anatomie, Software bis zur Perfektion auszuliefern.