
Одна кодова база, два застосунки: як зробити white-label без форку
GeekBye і Pavleur — це два по-різному брендовані десктопні застосунки, зібрані з одного репозиторію: без форку, без дубльованої кодової бази. Ось механіка часу збірки, яка перетворює одну кодову базу на два продукти, і однорядковий баг, через який наш другий застосунок представлявся неправильним іменем.
GeekBye — це нотатник для зустрічей. Pavleur — це AI-застосунок для тренування співбесід. Вони виглядають як два різні продукти, бо вони і є два різні продукти — але зібрані з однієї кодової бази, без форку й без гілки, що розповзається. Це інженерний бік white-labeling: не «навіщо продавати брендований застосунок» (це інший пост), а як ти насправді компілюєш один репозиторій у два застосунки — і непоказний баг, що ховається в кожній 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 і ніколи не задумується, який це бренд. - Копіює ресурси лише цього бренду — іконки, маленьке лого, іконку toast-сповіщень. Збірка GeekBye фізично не містить жодного арту Pavleur і навпаки. Нічому нема витікати.
- Переписує
package.json(на релізних збірках) — app id, ім'я продукту, URL-протокол, GitHub-репозиторій релізів для публікації і, що приємно, брендований текст дозволу на доступ до мікрофона, тож macOS питає «Pavleur хоче отримати доступ до мікрофона», а не неправильний бренд.
Весь застосунок бренд-агностичний; бренд впорскується під час збірки. Немає рантайм-перемикача продукту, який можна зіпсувати, бо в рантаймі завжди запечено лише один продукт.
Один backend, два бренди
Обидва застосунки звертаються до того самого backend. То як же сервер знає, якому продукту — і яким цінам, промптам та лімітам — належить запит?
Два сигнали, обидва задані під час збірки. Перший — application id (geekbye або pavleur), запечений у config і надісланий із запитами, тож 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-labeling — це не «чи змінилося лого». Це «чи проходить кожен рядок ідентичності через config». І останні недобитки ніколи не живуть в UI, де ти б їх шукав. Вони живуть у User-Agent, обробнику протоколу deep-link, тексті дозволу-запиту, повідомленнях про помилки — у сантехніці, яку ніхто не заносить у розділ «бренд». Єдиний надійний захист — ставитися до літерала бренду як до чогось, на що ти активно полюєш: грепни кодову базу на захардкоджене ім'я й зроби так, щоб збірка падала, якщо воно з'являється будь-де поза config, який має його визначати.
Три речі, яких нас навчив white-labeling
- Генеруй, а не гілкуй. Кодогенерація під час збірки двох крихітних config-модулів плюс вибіркове копіювання ресурсів б'є і форк, і рантайм-хащу перевірок продукту. Застосунок лишається бренд-агностичним; бренд — це вхідні дані збірки.
- Рядки ідентичності протікають у сантехніку. UI — це легкі 90%. Складні 10% — це User-Agent, URL-протокол, рядок дозволу на мікрофон, текст помилок. Проведи їх усі через один експорт
productName/applicationId— і грепай сире ім'я бренду як CI-гейт, щоб випадковий літерал ніколи не потрапив у реліз. - Один backend, багато брендів, розрізняй за id та заголовком. Заданий під час збірки application id плюс User-Agent виду
Product/version (platform)дають атрибуцію по продукту й по платформі з нульовою дубльованою інфраструктурою — чисто відокремлюючи незмінну ідентичність часу збірки від рантайм-поведінки.
Про продуктовий і приватний бік запуску white-label нотатника під власним брендом дивись white-label AI-нотатник. Це шостий розділ історії v1, яка стає GeekBye v2 — про попередній розділ читай, чим насправді є реліз зі 127 commitів (v1.7.0); а про всю дугу — анатомію доставки програмного забезпечення до досконалості.