Steven
Steven5 min leestijd

Eén codebase, twee apps: white-labelen zonder te forken

GeekBye en Pavleur zijn twee verschillend gebrande desktop-apps gebouwd vanuit één repository — geen fork, geen gedupliceerde codebase. Dit is de build-machinerie die één codebase naar twee producten laat compileren, en de éénregelige bug waardoor onze tweede app zich onder de verkeerde naam voorstelde.

Engineering
Architectuur
Build
GeekBye Releases
Eén codebase, twee apps: white-labelen zonder te forken

GeekBye is een meeting-notetaker. Pavleur is een AI-app om sollicitatiegesprekken te oefenen. Ze zien eruit als twee verschillende producten omdat het ook twee verschillende producten zijn — maar ze worden gebouwd vanuit één codebase, zonder fork en zonder branch die wegdrijft. Dit is de engineeringkant van white-labelen: niet "waarom zou je een gebrande app verkopen" (dat is een andere post), maar hoe je één repository daadwerkelijk naar twee apps compileert — en de weinig glamoureuze bug die zich in elk white-labelsysteem verstopt totdat je ernaar op zoek gaat.

Genereer, branch niet

De verkeerde manier om twee producten vanuit één codebase te draaien is if (product === 'pavleur') door de hele app strooien, of forken en bidden dat de twee kopieën nooit uit elkaar groeien. Beide rotten snel weg.

De aanpak van GeekBye is codegeneratie op buildtijd. Elk product heeft een configbestand — configs/geekbye/config.ts, configs/pavleur/config.ts — met zijn identiteit: naam, bundle-id, URL-protocol, de releases-repository voor auto-updates, enzovoort. Vóór een build draait één script:

node scripts/build-product.js pavleur

Dat script leest de config van het gekozen product en doet drie concrete dingen:

  1. Genereert twee configmodules — één voor de renderer (src/config/product.ts), één voor het main-proces (electron/config/product.ts) — elk gestempeld met een "DO NOT EDIT MANUALLY — changes will be overwritten"-header. De rest van de app importeert gewoon productName, applicationId en protocol uit de config en denkt nooit na over welk merk het is.
  2. Kopieert alleen de assets van dat merk — iconen, het kleine logo, het toast-notificatie-icoon. Een GeekBye-build bevat fysiek nul Pavleur-artwork en vice versa. Niets om te lekken.
  3. Herschrijft package.json (bij release-builds) — de app-id, de productnaam, het URL-protocol, de GitHub-releases-repo om naar te publiceren, en, mooi meegenomen, de gebrande microfoonpermissietekst, zodat macOS vraagt "Pavleur wil toegang tot de microfoon", niet het verkeerde merk.

De hele app is merk-agnostisch; het merk wordt op buildtijd geïnjecteerd. Er is geen runtime-productswitch om fout te zetten, want op runtime is er altijd maar één product ingebakken.

Eén backend, twee merken

Beide apps praten met dezelfde backend. Dus hoe weet de server bij welk product — en welke prijzen, prompts en limieten — een request hoort?

Twee signalen, beide gezet op buildtijd. Ten eerste een application id (geekbye of pavleur) ingebakken in de config en meegestuurd met requests, zodat de backend naar het gedrag van het juiste product kan routeren. Ten tweede een User-Agent die de client aan elke backend-call hangt, van de vorm Product/version (platform) — bijvoorbeeld Pavleur/1.2.5 (win32) of GeekBye/1.7.3 (darwin). Die ene header geeft de backend gratis attributie per product én per OS, zonder apart analytics-veld. Auto-updates blijven intussen geïsoleerd: elk product publiceert naar zijn eigen releases-repository, dus een GeekBye-update kan nooit aan een Pavleur-installatie worden geserveerd.

De onboarding verschilt ook per product — de wizard van GeekBye gaat over meetingtranscriptie, die van Pavleur over interviewoefening — en de juiste wordt op runtime gekozen op basis van het ene ingebakken application id. Dezelfde machinerie, twee voordeuren.

De bug: toen Pavleur zichzelf GeekBye noemde

Hier is het oorlogsverhaal, en het is er een waarvan elk white-labelsysteem een eigen versie heeft.

De configgeneratie werkte. De iconen wisselden. De microfoonprompt was gebrand. De protocollen en bundle-ids klopten. Op elke zichtbare maatstaf was een Pavleur-build Pavleur. En toch belde elke Pavleur-build een tijdlang naar huis bij de backend onder de naam GeekBye.

De boosdoener was één hardcoded string, en hij verstopte zich op de minst glamoureuze plek denkbaar: de User-Agent-builder, weggestopt in een netwerk-retry-helper. Alles wat de gebruiker te zien kreeg liep via de config, maar deze diep-in-het-loodgieterswerk-utility had nog steeds een letterlijke `GeekBye/${version}` in zich. Niemand denkt bij een retry-utility aan "branding", dus niemand had hem gecheckt. De fix was vier regels — de dynamische productName uit de config importeren en die interpoleren in plaats van het hardcoded merk — maar de les is waar het om draait:

White-labelen is niet "is het logo veranderd". Het is "loopt elke identiteitsstring door de config". En de laatste achterblijvers zitten nooit in de UI, waar je zou kijken. Ze zitten in de User-Agent, de deep-link-protocolhandler, de tekst van de permissieprompt, de foutmeldingen — het loodgieterswerk dat niemand onder "merk" archiveert. De enige betrouwbare verdediging is de merkliteral behandelen als iets waar je actief op jaagt: grep de codebase op de hardcoded naam en laat de build falen als hij ergens opduikt buiten de config die hem hoort te definiëren.

Drie dingen die white-labelen ons leerde

  1. Genereer, branch niet. Buildtijd-codegen van twee kleine configmodules plus selectieve asset-copy verslaat zowel forken als een runtime-woud aan productchecks. De app blijft merk-agnostisch; het merk is een build-input.
  2. Identiteitsstrings lekken naar het loodgieterswerk. De UI is de makkelijke 90%. De moeilijke 10% is de User-Agent, het URL-protocol, de microfoonpermissiestring, de fouttekst. Leid ze allemaal door één productName / applicationId-export — en grep op de kale merknaam als CI-gate zodat een verdwaalde literal nooit kan shippen.
  3. Eén backend, veel merken, onderscheid via id en header. Een application id van buildtijd plus een Product/version (platform) User-Agent geeft attributie per product en per platform met nul gedupliceerde infrastructuur — en scheidt de onveranderlijke buildtijd-identiteit netjes van runtime-gedrag.

Voor de product-en-privacykant van een white-label notetaker onder je eigen merk draaien, zie de white-label AI-notetaker. Dit is het zesde hoofdstuk van het v1-verhaal dat GeekBye v2 wordt — voor het vorige hoofdstuk, wat een release van 127 commits eigenlijk is (v1.7.0); en voor de hele boog, de anatomie van software tot in de perfectie uitbrengen.

Gerelateerde Artikelen

Eén CSS-variabele, vijf reviewrondes en een Swift-toolchain die loog
Steven
Steven7 min leestijd

Eén CSS-variabele, vijf reviewrondes en een Swift-toolchain die loog

GeekBye v2.0.7 maakte de hele overlay instelbaar doorschijnend — wat klinkt als een eenregelige CSS-wijziging en dat absoluut niet was. Het echte verhaal is wat de code review betrapte: twee oppervlakken die weigerden te vervagen, een opnamebalk die er verkeerd uitzag bij dezelfde opacity, en een gecompileerde binary die 672 bytes heen en weer sprong omdat onze eigen docs een reviewer de verkeerde Swift-versie vertelden.

Engineering
Design
Build
We schrapten 5.000 regels audiocode — en transcripten begonnen dubbel te verschijnen
Steven
Steven6 min leestijd

We schrapten 5.000 regels audiocode — en transcripten begonnen dubbel te verschijnen

GeekBye v1.6 rukte twee on-device Swift-transcribers eruit en verving ze door één verenigde pijplijn — netto meer dan 5.000 regels geschrapt, terwijl mensen midden in een meeting op de app zaten. Toen begonnen transcripten dubbel te verschijnen, omdat de bug verhuisde naar de ene laag die nog dacht dat er twee engines waren.

Engineering
Transcriptie
Architectuur
Wat een release van 127 commits eigenlijk is
Steven
Steven6 min leestijd

Wat een release van 127 commits eigenlijk is

GeekBye v1.7.0 was 127 commits in elf dagen. Van buitenaf lijkt dat op honderd kleine dingetjes. Van binnenuit waren het twee grote features die aan het eind in elkaar werden gevlochten — en één ervan werd op de verkeerde plek gebouwd, om vervolgens midden in de release eruit gescheurd en opnieuw opgebouwd te worden. Dit is de anatomie van een grote release.

Engineering
Release
Architectuur