Steven
Steven5 мин. чтения

Одна кодовая база, два приложения: как сделать white-label без форка

GeekBye и Pavleur — это два по-разному брендированных десктопных приложения, собранных из одного репозитория: без форка, без дублирования кодовой базы. Вот механика на этапе сборки, которая превращает одну кодовую базу в два продукта, и однострочный баг, из-за которого наше второе приложение представлялось не тем именем.

Инженерия
Архитектура
Сборка
Релизы GeekBye
Одна кодовая база, два приложения: как сделать white-label без форка

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

Этот скрипт читает конфиг выбранного продукта и делает три конкретные вещи:

  1. Генерирует два config-модуля — один для рендерера (src/config/product.ts), один для главного процесса (electron/config/product.ts) — каждый со штампом-заголовком «DO NOT EDIT MANUALLY — changes will be overwritten». Остальная часть приложения просто импортирует productName, applicationId и protocol из config и никогда не задумывается, каким брендом она является.
  2. Копирует ассеты только этого бренда — иконки, маленький логотип, иконку для тост-уведомлений. Сборка GeekBye физически содержит ноль графики Pavleur, и наоборот. Утекать нечему.
  3. Переписывает 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

  1. Генерируй, а не ветвись. Генерация двух крошечных config-модулей на этапе сборки плюс выборочное копирование ассетов бьёт и форк, и рантайм-заросли продуктовых проверок. Приложение остаётся бренд-агностичным; бренд — это вход сборки.
  2. Строки идентичности утекают в водопровод. Интерфейс — это лёгкие 90%. Тяжёлые 10% — это User-Agent, URL-протокол, строка разрешения на микрофон, текст ошибок. Пропусти их все через один экспорт productName / applicationId — и грепай сырое имя бренда как CI-гейт, чтобы залётный литерал никогда не смог отгрузиться.
  3. Один backend, много брендов, различай по id и заголовку. Application id, заданный на этапе сборки, плюс User-Agent вида Product/version (platform) дают атрибуцию по продукту и по платформе с нулём дублированной инфраструктуры — чисто разделяя неизменную идентичность этапа сборки и рантайм-поведение.

Про продуктовую и приватную сторону запуска white-label заметочника под собственным брендом смотри white-label заметочник с ИИ. Это шестая глава истории v1, которая становится GeekBye v2 — предыдущую главу смотри в чем на самом деле является релиз из 127 коммитов (v1.7.0); а весь путь — в анатомии доведения софта до совершенства.

Похожие статьи

Одна CSS-переменная, пять раундов ревью и Swift-тулчейн, который солгал
Steven
Steven6 мин. чтения

Одна CSS-переменная, пять раундов ревью и Swift-тулчейн, который солгал

GeekBye v2.0.7 сделал весь оверлей регулируемо полупрозрачным — звучит как однострочное изменение в CSS и совершенно им не является. Настоящая история — в том, что поймало код-ревью: две поверхности, которые отказывались растворяться, панель записи, которая при той же прозрачности выглядела не так, и скомпилированный бинарник, который туда-сюда менял 672 байта, потому что наша же документация назвала ревьюеру не ту версию Swift.

Инженерия
Дизайн
Сборка
Мы удалили 5 000 строк аудиокода — и транскрипты начали появляться дважды
Steven
Steven5 мин. чтения

Мы удалили 5 000 строк аудиокода — и транскрипты начали появляться дважды

GeekBye v1.6 выдрал два транскрайбера на устройстве, написанных на Swift, и заменил их одним унифицированным конвейером — чистое удаление более 5 000 строк, сделанное прямо во время того, как люди сидели на встречах в приложении. А потом транскрипты стали появляться дважды, потому что баг переехал в единственный слой, который всё ещё думал, что движков два.

Инженерия
Транскрипция
Архитектура
Что на самом деле представляет собой релиз из 127 коммитов
Steven
Steven5 мин. чтения

Что на самом деле представляет собой релиз из 127 коммитов

GeekBye v1.7.0 — это 127 коммитов за одиннадцать дней. Снаружи это выглядит как сотня мелочей. Изнутри это были две крупные функции, сплетённые вместе, — и одну из них сначала построили не в том месте, потом вырвали и переделали посреди релиза. Вот анатомия крупного релиза.

Инженерия
Релиз
Архитектура