
Jedna baza kodu, dwie aplikacje: jak robić white-label bez forkowania
GeekBye i Pavleur to dwie odmiennie zbrandowane aplikacje desktopowe zbudowane z jednego repozytorium — bez forka, bez zduplikowanej bazy kodu. Oto mechanika czasu budowania, która sprawia, że jedna baza kodu kompiluje się do dwóch produktów, oraz jednolinijkowy błąd, przez który nasza druga aplikacja przedstawiała się pod niewłaściwą nazwą.
GeekBye to notatnik ze spotkań. Pavleur to aplikacja do treningu rozmów kwalifikacyjnych z AI. Wyglądają jak dwa różne produkty, bo są dwoma różnymi produktami — ale zbudowano je z jednej bazy kodu, bez forka i bez gałęzi, która dryfuje. To inżynierska strona white-labelingu: nie „dlaczego sprzedawać zbrandowaną aplikację" (o tym jest inny wpis), lecz jak faktycznie skompilować jedno repozytorium do dwóch aplikacji — oraz mało efektowny błąd, który chowa się w każdym systemie white-label, dopóki go nie poszukasz.
Generuj, nie rozgałęziaj
Złym sposobem na prowadzenie dwóch produktów z jednej bazy kodu jest rozsianie if (product === 'pavleur') po całej aplikacji albo forkowanie i modlenie się, żeby dwie kopie nigdy się nie rozjechały. Oba psują się szybko.
Podejście GeekBye to generowanie kodu w czasie budowania. Każdy produkt ma plik config — configs/geekbye/config.ts, configs/pavleur/config.ts — który trzyma jego tożsamość: nazwę, bundle id, protokół URL, repozytorium wydań dla auto-aktualizacji i tak dalej. Przed buildem uruchamia się jeden skrypt:
node scripts/build-product.js pavleur
Ten skrypt czyta config wybranego produktu i robi trzy konkretne rzeczy:
- Generuje dwa moduły config — jeden dla renderera (
src/config/product.ts), jeden dla procesu głównego (electron/config/product.ts) — każdy oznaczony nagłówkiem „DO NOT EDIT MANUALLY — changes will be overwritten". Reszta aplikacji po prostu importujeproductName,applicationIdiprotocolz config i nigdy nie zastanawia się, którą jest marką. - Kopiuje tylko zasoby danej marki — ikony, małe logo, ikonę powiadomień toast. Build GeekBye fizycznie nie zawiera ani jednej grafiki Pavleur i odwrotnie. Nie ma czemu wyciec.
- Przepisuje
package.json(przy buildach wydań) — app id, nazwę produktu, protokół URL, repozytorium wydań GitHub, do którego publikować, oraz, co miłe, zbrandowany tekst zgody na dostęp do mikrofonu, tak by macOS pytał „Pavleur chce uzyskać dostęp do mikrofonu", a nie pod niewłaściwą marką.
Cała aplikacja jest niezależna od marki; marka jest wstrzykiwana w czasie budowania. Nie ma runtime''owego przełącznika produktu, który można by zepsuć, bo w runtime jest tylko jeden produkt wypieczony na stałe.
Jeden backend, dwie marki
Obie aplikacje rozmawiają z tym samym backendem. Skąd więc serwer wie, do którego produktu — i którego cennika, promptów i limitów — należy żądanie?
Dwa sygnały, oba ustawiane w czasie budowania. Po pierwsze, application id (geekbye lub pavleur) wypieczone w config i wysyłane z żądaniami, żeby backend mógł skierować je do zachowania właściwego produktu. Po drugie, User-Agent, który klient dołącza do każdego wywołania backendu, w kształcie Product/version (platform) — np. Pavleur/1.2.5 (win32) czy GeekBye/1.7.3 (darwin). Ten jeden nagłówek daje backendowi atrybucję per produkt oraz per system operacyjny za darmo, bez osobnego pola analitycznego. Tymczasem auto-aktualizacje pozostają odizolowane: każdy produkt publikuje do własnego repozytorium wydań, więc aktualizacja GeekBye nigdy nie może zostać podana instalacji Pavleur.
Onboarding też różni się per produkt — kreator GeekBye dotyczy transkrypcji spotkań, a Pavleur treningu rozmów kwalifikacyjnych — i właściwy jest wybierany w runtime na podstawie jednego wypieczonego application id. Ta sama mechanika, dwoje drzwi frontowych.
Błąd: gdy Pavleur nazywał sam siebie GeekBye
Oto historia z pola bitwy, a każdy system white-label ma jej jakąś wersję.
Generowanie configu działało. Ikony się podmieniły. Prompt mikrofonu był zbrandowany. Protokoły i bundle id były poprawne. Według każdej widocznej miary build Pavleur był Pavleurem. A mimo to, przez pewien czas, każdy build Pavleur dzwonił do domu, do backendu, nazywając się GeekBye.
Winowajcą był pojedynczy zaszyty na sztywno ciąg, a chował się w najmniej efektownym miejscu, jakie można sobie wyobrazić: w builderze User-Agent, wciśniętym w sieciowy helper ponawiania. Wszystko widoczne dla użytkownika zostało przepuszczone przez config, ale to jedno narzędzie z głębin hydrauliki wciąż miało w sobie literał `GeekBye/${version}`. Nikt nie myśli o narzędziu do ponawiania jak o „brandingu", więc nikt go nie sprawdził. Poprawka miała cztery linie — zaimportować dynamiczne productName z config i wstawić je w miejsce zaszytej na sztywno marki — ale to właśnie lekcja jest tu całym sednem:
White-labeling to nie „czy logo się zmieniło". To „czy każdy ciąg tożsamości przechodzi przez config". A ostatnie niedobitki nigdy nie mieszkają w UI, gdzie byś szukał. Mieszkają w User-Agent, w handlerze protokołu deep-link, w tekście zgody uprawnień, w komunikatach o błędach — w hydraulice, której nikt nie klasyfikuje pod „marką". Jedyną niezawodną obroną jest traktowanie literału marki jako czegoś, na co aktywnie polujesz: przeszukaj bazę kodu grepem pod kątem zaszytej na sztywno nazwy i wywal build, jeśli pojawi się gdziekolwiek poza configiem, który ma ją definiować.
Trzy rzeczy, których nauczył nas white-labeling
- Generuj, nie rozgałęziaj. Generowanie w czasie budowania dwóch maleńkich modułów config plus selektywne kopiowanie zasobów bije zarówno forkowanie, jak i runtime''owy gąszcz sprawdzeń produktu. Aplikacja pozostaje niezależna od marki; marka jest wejściem buildu.
- Ciągi tożsamości wyciekają do hydrauliki. UI to łatwe 90%. Trudne 10% to User-Agent, protokół URL, tekst zgody na mikrofon, komunikaty o błędach. Przepuść je wszystkie przez jeden eksport
productName/applicationId— i grepuj surową nazwę marki jako bramkę CI, żeby zabłąkany literał nigdy nie mógł trafić do wydania. - Jeden backend, wiele marek, rozróżniaj po id i nagłówku. Ustalane w czasie budowania application id plus User-Agent
Product/version (platform)daje atrybucję per produkt i per platforma bez ani jednej zduplikowanej infrastruktury — czysto oddzielając niezmienną tożsamość z czasu budowania od zachowania w runtime.
Po produktowo-prywatnościową stronę prowadzenia notatnika white-label pod własną marką zobacz notatnik AI w modelu white-label. To szósty rozdział historii v1, która staje się GeekBye v2 — po poprzedni rozdział zobacz czym naprawdę jest wydanie ze 127 commitów (v1.7.0); a po cały łuk, anatomię dostarczania oprogramowania do perfekcji.