
Одна кодовая база, два приложения: как сделать white-label без форка
GeekBye и Pavleur — это два по-разному брендированных десктопных приложения, собранных из одного репозитория: без форка, без дублирования кодовой базы. Вот механика на этапе сборки, которая превращает одну кодовую базу в два продукта, и однострочный баг, из-за которого наше второе приложение представлялось не тем именем.
GeekBye — это заметочник для встреч. Pavleur — это приложение для тренировки собеседований с ИИ. Они выглядят как два разных продукта, потому что они и есть два разных продукта — но собраны они из одной кодовой базы, без форка и без ветки, которая расходится. Это инженерная сторона white-label: не «зачем продавать брендированное приложение» (это другой пост), а как ты на самом деле компилируешь один репозиторий в два приложения — и неприглядный баг, который прячется в любой системе white-label, пока ты не пойдёшь его искать.
Генерируй, а не ветвись
Неправильный способ гонять два продукта из одной кодовой базы — рассыпать if (product === 'pavleur') по всему приложению или сделать форк и молиться, чтобы две копии никогда не разошлись. И то, и другое быстро гниёт.
Подход GeekBye — это генерация кода на этапе сборки. У каждого продукта есть свой конфиг-файл — configs/geekbye/config.ts, configs/pavleur/config.ts — который держит его идентичность: имя, bundle id, URL-протокол, репозиторий релизов для автообновлений и так далее. Перед сборкой запускается один скрипт:
node scripts/build-product.js pavleur
Этот скрипт читает конфиг выбранного продукта и делает три конкретные вещи:
- Генерирует два config-модуля — один для рендерера (
src/config/product.ts), один для главного процесса (electron/config/product.ts) — каждый со штампом-заголовком «DO NOT EDIT MANUALLY — changes will be overwritten». Остальная часть приложения просто импортируетproductName,applicationIdиprotocolиз config и никогда не задумывается, каким брендом она является. - Копирует ассеты только этого бренда — иконки, маленький логотип, иконку для тост-уведомлений. Сборка GeekBye физически содержит ноль графики Pavleur, и наоборот. Утекать нечему.
- Переписывает
package.json(на релизных сборках) — app id, имя продукта, URL-протокол, репозиторий релизов на GitHub для публикации и, что приятно, брендированный текст разрешения на микрофон, чтобы macOS спрашивал «Pavleur хочет получить доступ к микрофону», а не с не тем брендом.
Всё приложение бренд-агностично; бренд внедряется на этапе сборки. Нет рантайм-переключателя продукта, который можно испортить, потому что в рантайме всегда запечён только один продукт.
Один backend, два бренда
Оба приложения общаются с одним и тем же backend. Так как же сервер узнаёт, какому продукту — и какому ценообразованию, промптам и лимитам — принадлежит запрос?
Два сигнала, оба задаются на этапе сборки. Первый — 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 была Pavleur. И всё же какое-то время каждая сборка Pavleur звонила домой на backend, называя себя GeekBye.
Виновником была единственная захардкоженная строка, и пряталась она в самом неприглядном месте, какое только можно вообразить: в билдере User-Agent, спрятанном внутри сетевого хелпера повторов. Всё, что видит пользователь, было пропущено через config, но эта одна глубоко-водопроводная утилита всё ещё держала в себе литерал `GeekBye/${version}`. Никто не думает о хелпере повторов как о «брендинге», поэтому его никто и не проверял. Фикс был в четыре строки — импортировать динамический productName из config и подставить его вместо захардкоженного бренда — но урок и есть весь смысл:
White-label — это не «сменился ли логотип». Это «проходит ли каждая строка идентичности через config». И последние отставшие никогда не живут в интерфейсе, где ты бы стал искать. Они живут в User-Agent, в обработчике протокола диплинков, в тексте разрешений, в сообщениях об ошибках — в водопроводе, который никто не относит к «бренду». Единственная надёжная защита — относиться к литералу бренда как к тому, за чем ты активно охотишься: грепать кодовую базу на захардкоженное имя и валить сборку, если оно появляется где угодно вне того config, который должен его определять.
Три вещи, которым нас научил white-label
- Генерируй, а не ветвись. Генерация двух крошечных config-модулей на этапе сборки плюс выборочное копирование ассетов бьёт и форк, и рантайм-заросли продуктовых проверок. Приложение остаётся бренд-агностичным; бренд — это вход сборки.
- Строки идентичности утекают в водопровод. Интерфейс — это лёгкие 90%. Тяжёлые 10% — это User-Agent, URL-протокол, строка разрешения на микрофон, текст ошибок. Пропусти их все через один экспорт
productName/applicationId— и грепай сырое имя бренда как CI-гейт, чтобы залётный литерал никогда не смог отгрузиться. - Один backend, много брендов, различай по id и заголовку. Application id, заданный на этапе сборки, плюс User-Agent вида
Product/version (platform)дают атрибуцию по продукту и по платформе с нулём дублированной инфраструктуры — чисто разделяя неизменную идентичность этапа сборки и рантайм-поведение.
Про продуктовую и приватную сторону запуска white-label заметочника под собственным брендом смотри white-label заметочник с ИИ. Это шестая глава истории v1, которая становится GeekBye v2 — предыдущую главу смотри в чем на самом деле является релиз из 127 коммитов (v1.7.0); а весь путь — в анатомии доведения софта до совершенства.