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); а весь путь — в анатомии доведения софта до совершенства.

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

Убираем backend с пути загрузки
Steven
Steven7 мин. чтения

Убираем backend с пути загрузки

GeekBye записывает твой экран и сохраняет видео в твой Google Drive. Первая версия прогоняла каждую запись через собственные серверы GeekBye по пути туда; релизом позже файл шёл прямо с твоей машины на Drive, а backend был понижен до хранения единственного указателя. Интересная часть в том, как мало кода на самом деле содержит «прямая, возобновляемая» версия — потому что возобновляемость пришла из удаления proxy, а не из его написания.

Инженерия
Архитектура
Десктоп
Одна 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 строк, сделанное прямо во время того, как люди сидели на встречах в приложении. А потом транскрипты стали появляться дважды, потому что баг переехал в единственный слой, который всё ещё думал, что движков два.

Инженерия
Транскрипция
Архитектура