Steven
Steven5 хв читання

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

GeekBye і Pavleur — це два по-різному брендовані десктопні застосунки, зібрані з одного репозиторію: без форку, без дубльованої кодової бази. Ось механіка часу збірки, яка перетворює одну кодову базу на два продукти, і однорядковий баг, через який наш другий застосунок представлявся неправильним іменем.

Інженерія
Архітектура
Збірка
Релізи GeekBye
Одна кодова база, два застосунки: як зробити white-label без форку

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

Цей скрипт читає конфіг обраного продукту й робить три конкретні речі:

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

  1. Генеруй, а не гілкуй. Кодогенерація під час збірки двох крихітних config-модулів плюс вибіркове копіювання ресурсів б'є і форк, і рантайм-хащу перевірок продукту. Застосунок лишається бренд-агностичним; бренд — це вхідні дані збірки.
  2. Рядки ідентичності протікають у сантехніку. UI — це легкі 90%. Складні 10% — це User-Agent, URL-протокол, рядок дозволу на мікрофон, текст помилок. Проведи їх усі через один експорт productName / applicationId — і грепай сире ім'я бренду як CI-гейт, щоб випадковий літерал ніколи не потрапив у реліз.
  3. Один backend, багато брендів, розрізняй за id та заголовком. Заданий під час збірки application id плюс User-Agent виду Product/version (platform) дають атрибуцію по продукту й по платформі з нульовою дубльованою інфраструктурою — чисто відокремлюючи незмінну ідентичність часу збірки від рантайм-поведінки.

Про продуктовий і приватний бік запуску white-label нотатника під власним брендом дивись white-label AI-нотатник. Це шостий розділ історії v1, яка стає GeekBye v2 — про попередній розділ читай, чим насправді є реліз зі 127 commitів (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 видалених рядків, зроблено просто тоді, коли люди були посеред зустрічі в застосунку. А потім транскрипти почали з'являтися двічі, бо баг переїхав на той єдиний шар, який досі вважав, що двигунів два.

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