
O singură bază de cod, două aplicații: cum faci white-label fără fork
GeekBye și Pavleur sunt două aplicații desktop cu branding diferit, construite dintr-un singur repository — fără fork, fără bază de cod duplicată. Iată mecanismul de build-time care face ca o singură bază de cod să compileze în două produse și bug-ul de o linie care a făcut ca a doua noastră aplicație să se prezinte cu numele greșit.
GeekBye este un notetaker de ședințe. Pavleur este o aplicație de exersare a interviurilor cu AI. Par două produse diferite pentru că sunt două produse diferite — dar sunt construite dintr-o singură bază de cod, fără fork și fără vreun branch care să se abată. Aceasta e partea de inginerie a white-label-ului: nu „de ce să vinzi o aplicație branded" (asta e altă postare), ci cum compilezi de fapt un singur repository în două aplicații — și bug-ul lipsit de glamour care se ascunde în orice sistem de white-label până te apuci să-l cauți.
Generează, nu ramifica
Modul greșit de a rula două produse dintr-o singură bază de cod este să presari if (product === 'pavleur') prin toată aplicația sau să faci fork și să te rogi ca cele două copii să nu diverge niciodată. Ambele se strică repede.
Abordarea GeekBye este generarea de cod la build-time. Fiecare produs are un fișier de config — configs/geekbye/config.ts, configs/pavleur/config.ts — care îi conține identitatea: nume, bundle id, protocol URL, repository-ul de lansări pentru auto-update și așa mai departe. Înainte de un build, rulează un singur script:
node scripts/build-product.js pavleur
Scriptul acela citește config-ul produsului ales și face trei lucruri concrete:
- Generează două module de config — unul pentru renderer (
src/config/product.ts), unul pentru procesul principal (electron/config/product.ts) — fiecare marcat cu un antet „DO NOT EDIT MANUALLY — changes will be overwritten". Restul aplicației doar importăproductName,applicationIdșiprotocoldin config și nu se gândește niciodată ce brand este. - Copiază doar assets-urile acelui brand — iconițe, logo-ul mic, iconița pentru notificări toast. Un build de GeekBye conține fizic zero grafică Pavleur și viceversa. Nimic de scurs.
- Rescrie
package.json(la build-urile de release) — app id-ul, numele produsului, protocolul URL, repo-ul de lansări GitHub în care să publice și, foarte plăcut, textul branded al permisiunii pentru microfon, astfel încât macOS să întrebe „Pavleur ar dori să acceseze microfonul", nu brandul greșit.
Întreaga aplicație e agnostică față de brand; brandul e injectat la build-time. Nu există niciun switch de produs la runtime care să poată fi greșit, pentru că la runtime există mereu doar un singur produs încorporat.
Un backend, două branduri
Ambele aplicații vorbesc cu același backend. Deci cum știe serverul cărui produs — și cărei scheme de prețuri, prompturi și limite — îi aparține o cerere?
Două semnale, ambele setate la build-time. Primul, un application id (geekbye sau pavleur) încorporat în config și trimis cu cererile, astfel încât backend-ul să poată direcționa către comportamentul produsului potrivit. Al doilea, un User-Agent pe care clientul îl atașează la fiecare apel către backend, de forma Product/version (platform) — de ex. Pavleur/1.2.5 (win32) sau GeekBye/1.7.3 (darwin). Acel singur header îi oferă backend-ului atribuire per produs și per sistem de operare gratuit, fără un câmp de analytics separat. Între timp, auto-update-urile rămân izolate: fiecare produs publică în propriul repository de lansări, așa că un update de GeekBye nu poate ajunge niciodată la o instalare de Pavleur.
Și onboarding-ul diferă de la produs la produs — wizard-ul GeekBye e despre transcrierea ședințelor, cel al lui Pavleur e despre exersarea interviurilor — iar cel potrivit e ales la runtime din singurul application id încorporat. Aceeași mașinărie, două uși de intrare.
Bug-ul: când Pavleur s-a numit pe sine GeekBye
Iată povestea de război, și e cea de care orice sistem de white-label are o variantă.
Generarea config-ului funcționa. Iconițele se schimbau. Prompt-ul pentru microfon era branded. Protocoalele și bundle id-urile erau corecte. După orice măsură vizibilă, un build de Pavleur era Pavleur. Și totuși, o vreme, fiecare build de Pavleur suna acasă la backend numindu-se GeekBye.
Vinovatul era un singur string hardcodat, și se ascundea în locul cel mai lipsit de glamour cu putință: builder-ul de User-Agent, îndesat într-un helper de retry de rețea. Tot ce era vizibil pentru utilizator fusese rutat prin config, dar acest singur utilitar de plumbing adânc mai avea încă un literal `GeekBye/${version}` în el. Nimeni nu se gândește la un utilitar de retry ca la „branding", așa că nimeni nu-l verificase. Reparația a însemnat patru linii — importă productName dinamic din config și interpolează-l în loc de brandul hardcodat — dar lecția e chiar esența:
White-label-ul nu înseamnă „s-a schimbat logo-ul". Înseamnă „trece fiecare string de identitate prin config". Iar ultimele rămășițe nu locuiesc niciodată în interfață, acolo unde te-ai uita. Locuiesc în User-Agent, în handler-ul de protocol pentru deep-link, în textul prompt-ului de permisiune, în mesajele de eroare — plumbing-ul pe care nimeni nu-l clasează la „brand". Singura apărare de încredere e să tratezi literalul de brand ca pe ceva ce vânezi activ: dă grep prin codebase după numele hardcodat și fă build-ul să eșueze dacă apare oriunde în afara config-ului care ar trebui să-l definească.
Trei lucruri pe care ni le-a predat white-label-ul
- Generează, nu ramifica. Generarea de cod la build-time a două module de config minuscule plus copierea selectivă a assets-urilor bate atât fork-ul, cât și un hățiș de verificări de produs la runtime. Aplicația rămâne agnostică față de brand; brandul e un input de build.
- Stringurile de identitate se scurg în plumbing. Interfața e cei 90% ușori. Cei 10% grei sunt User-Agent-ul, protocolul URL, stringul de permisiune pentru microfon, textul erorilor. Rutează-le pe toate printr-un singur export
productName/applicationId— și dă grep după numele brut al brandului ca poartă de CI, astfel încât un literal rătăcit să nu poată ajunge niciodată în producție. - Un backend, multe branduri, dezambiguizate prin id și header. Un application id de build-time plus un User-Agent
Product/version (platform)oferă atribuire per produs și per platformă cu zero infrastructură duplicată — separând curat identitatea imutabilă de build-time de comportamentul de runtime.
Pentru partea de produs și de confidențialitate a rulării unui notetaker white-label sub propriul brand, vezi notetaker-ul AI white-label. Acesta e al șaselea capitol al poveștii v1 care devine GeekBye v2 — pentru capitolul anterior, ce înseamnă cu adevărat o lansare de 127 de commit-uri (v1.7.0); iar pentru întregul arc, anatomia livrării de software până la perfecțiune.