
Една кодова база, две приложения: как да направиш white-label без fork
GeekBye и Pavleur са две различно брандирани десктоп приложения, изградени от едно-единствено хранилище — без fork, без дублирана кодова база. Ето машинарията по време на build, която компилира една кодова база в два продукта, и бъгът от един ред, заради който второто ни приложение се представяше с грешното име.
GeekBye е бележник за срещи. Pavleur е AI приложение за упражняване на интервюта. Изглеждат като два различни продукта, защото са два различни продукта — но са изградени от една кодова база, без fork и без клон, който бавно се разминава. Това е инженерната страна на white-label подхода: не „защо да продаваш брандирано приложение" (това е друг пост), а как всъщност компилираш едно хранилище в две приложения — и непривлекателният бъг, който се крие във всяка white-label система, докато не тръгнеш да го търсиш.
Генерирай, не разклонявай
Грешният начин да караш два продукта от една кодова база е да поръсиш if (product === 'pavleur') из цялото приложение или да направиш fork и да се молиш двете копия никога да не се разминат. И двете загниват бързо.
Подходът на GeekBye е генериране на код по време на build. Всеки продукт има конфигурационен файл — configs/geekbye/config.ts, configs/pavleur/config.ts — съдържащ идентичността му: име, bundle id, URL протокол, хранилището с издания за автоматични обновявания и така нататък. Преди build се изпълнява един скрипт:
node scripts/build-product.js pavleur
Този скрипт чете конфигурацията на избрания продукт и прави три конкретни неща:
- Генерира два конфигурационни модула — един за renderer-а (
src/config/product.ts) и един за main процеса (electron/config/product.ts) — всеки подпечатан с хедър „НЕ РЕДАКТИРАЙ РЪЧНО — промените ще бъдат презаписани". Останалата част от приложението просто импортираproductName,applicationIdиprotocolот конфигурацията и никога не мисли кой бранд е. - Копира само ресурсите на този бранд — иконите, малкото лого, иконата за toast известия. Един GeekBye build физически не съдържа нито един пиксел Pavleur графика и обратно. Няма какво да изтече.
- Пренаписва
package.json(при release build-ове) — app id-то, името на продукта, URL протокола, GitHub хранилището с издания, в което се публикува, и, което е приятно, брандирания текст за разрешение за микрофона, така че macOS да пита „Pavleur иска достъп до микрофона", а не грешния бранд.
Цялото приложение е агностично към бранда; брандът се инжектира по време на build. Няма runtime превключвател на продукти, който да сгрешиш, защото по време на изпълнение винаги има само един вграден продукт.
Един backend, два бранда
Двете приложения говорят с един и същ backend. Как тогава сървърът знае към кой продукт — и към кои цени, промптове и лимити — принадлежи една заявка?
Два сигнала, и двата зададени по време на build. Първо, application id (geekbye или pavleur), вграден в конфигурацията и изпращан със заявките, така че backend-ът да може да насочи към поведението на правилния продукт. Второ, User-Agent, който клиентът прикача към всяко обръщение към backend-а, от вида Product/version (platform) — напр. Pavleur/1.2.5 (win32) или GeekBye/1.7.3 (darwin). Този един хедър дава на backend-а атрибуция по продукт и по операционна система безплатно, без отделно поле за аналитика. Автоматичните обновявания междувременно остават изолирани: всеки продукт публикува в собственото си хранилище с издания, така че обновяване на GeekBye никога не може да бъде сервирано на инсталация на Pavleur.
Онбордингът също се различава по продукт — уизардът на GeekBye е за транскрипция на срещи, този на Pavleur — за упражняване на интервюта — и правилният се избира по време на изпълнение от единствения вграден application id. Една и съща машинария, две входни врати.
Бъгът: когато Pavleur се нарече GeekBye
Ето историята от фронта — и това е историята, чиято версия има всяка white-label система.
Генерирането на конфигурация работеше. Иконите се сменяха. Подканата за микрофона беше брандирана. Протоколите и bundle id-тата бяха правилни. По всяка видима мярка един Pavleur build беше Pavleur. И въпреки това, за известно време, всеки Pavleur build се обаждаше на backend-а, наричайки се GeekBye.
Виновникът беше един-единствен хардкоднат низ, скрит на най-непривлекателното място, което можеш да си представиш: строителя на User-Agent, сгушен в мрежов retry помощник. Всичко видимо за потребителя беше прекарано през конфигурацията, но тази една дълбоко заровена инфраструктурна помощна функция все още имаше литерал `GeekBye/${version}` в себе си. Никой не мисли за retry функция като за „брандиране", затова никой не я беше проверил. Поправката беше четири реда — импортирай динамичното productName от конфигурацията и интерполирай него вместо хардкоднатия бранд — но поуката е цялата идея:
White-label не е „смени ли се логото". А „минава ли всеки низ за идентичност през конфигурацията". И последните упорити остатъци никога не живеят в UI-я, където би погледнал. Живеят в User-Agent, в обработчика на deep-link протокола, в текста на подканата за разрешения, в съобщенията за грешки — в тръбопровода, който никой не картотекира като „бранд". Единствената надеждна защита е да третираш бранд литерала като нещо, което активно преследваш: grep-ни кодовата база за хардкоднатото име и проваляй build-а, ако се появи където и да е извън конфигурацията, която трябва да го дефинира.
Три неща, на които ни научи white-label подходът
- Генерирай, не разклонявай. Codegen по време на build на два мънички конфигурационни модула плюс селективно копиране на ресурси бие и fork-а, и runtime гъсталака от продуктови проверки. Приложението остава агностично към бранда; брандът е входен параметър на build-а.
- Низовете за идентичност изтичат в тръбопровода. UI-ят е лесните 90%. Трудните 10% са User-Agent, URL протоколът, низът за разрешение за микрофона, текстовете на грешките. Прекарай ги всичките през един export на
productName/applicationId— и grep-вай за суровото име на бранда като CI бариера, така че заблуден литерал никога да не може да стигне до потребителите. - Един backend, много брандове, различавай по id и хедър. Application id от build време плюс User-Agent от вида
Product/version (platform)дава атрибуция по продукт и по платформа с нула дублирана инфраструктура — чисто разделяйки неизменната идентичност от build време от поведението по време на изпълнение.
За продуктово-приватната страна на това да караш white-label бележник под собствения си бранд виж white-label AI бележника. Това е шестата глава от историята на v1, която се превръща в GeekBye v2 — за предишната глава, какво всъщност представлява едно издание със 127 комита (v1.7.0); а за цялата дъга, анатомията на доставянето на софтуер до съвършенство.