
一つのコードベース、二つのアプリ: フォークせずにホワイトラベル化する方法
GeekByeとPavleurは、単一のリポジトリから作られる、別々にブランド付けされた二つのデスクトップアプリです — フォークもなく、コードベースの複製もありません。これが、一つのコードベースを二つのプロダクトへコンパイルさせるビルド時の仕組みであり、そして私たちの二つ目のアプリが間違った名前で自己紹介してしまった一行のバグの話です。
GeekByeはミーティングのノートテイカーです。PavleurはAIによる面接練習アプリです。両者は二つの異なるプロダクトのように見えます — なぜなら両者は本当に二つの異なるプロダクトだからです — しかし両者は一つのコードベースから作られており、フォークもなければ、ずれていくブランチもありません。これがホワイトラベル化のエンジニアリング側です: 「なぜブランド付きアプリを売るのか」ではなく(それは別の記事です)、実際にどうやって一つのリポジトリを二つのアプリにコンパイルするのか — そして、探しに行くまであらゆるホワイトラベルシステムに潜んでいる、地味なバグの話です。
分岐するな、生成せよ
一つのコードベースから二つのプロダクトを走らせる間違ったやり方は、アプリのあちこちにif (product === 'pavleur')をばらまくか、フォークして二つのコピーが決してずれないよう祈ることです。どちらも速く腐ります。
GeekByeのアプローチはビルド時のコード生成です。各プロダクトはconfigファイルを持ちます — configs/geekbye/config.ts、configs/pavleur/config.ts — その識別情報を保持しています: 名前、バンドルid、URLプロトコル、自動更新用のリリースリポジトリ、などなど。ビルドの前に、一つのスクリプトが走ります:
node scripts/build-product.js pavleur
そのスクリプトは選ばれたプロダクトのconfigを読み、三つの具体的なことをします:
- 二つのconfigモジュールを生成する — 一つはレンダラー用(
src/config/product.ts)、一つはメインプロセス用(electron/config/product.ts) — それぞれに*「手動で編集するな — 変更は上書きされる」*というヘッダーが刻印されます。アプリの残りはただconfigからproductName、applicationId、protocolをインポートするだけで、自分がどのブランドなのかを一切考えません。 - そのブランドのアセットだけをコピーする — アイコン、小さなロゴ、トースト通知アイコン。GeekByeビルドは物理的にPavleurの絵素材をゼロしか含まず、逆もまた然り。漏れるものは何もありません。
package.jsonを書き換える(リリースビルドで) — アプリid、プロダクト名、URLプロトコル、公開先のGitHubリリースリポジトリ、そして嬉しいことに、ブランド付けされたマイク許可テキストまで — macOSが「Pavleurがマイクへのアクセスを求めています」と尋ね、間違ったブランドを出さないように。
アプリ全体はブランド非依存であり、ブランドはビルド時に注入されます。ランタイムのプロダクト切り替えを間違える余地はありません。なぜならランタイムには、焼き込まれた一つのプロダクトしか存在しないからです。
一つのbackend、二つのブランド
両アプリは同じbackendと話します。では、あるリクエストがどのプロダクト — そしてどの料金、プロンプト、制限 — に属するのかを、サーバーはどうやって知るのでしょうか?
二つのシグナルがあり、どちらもビルド時に設定されます。第一に、configに焼き込まれてリクエストとともに送られるapplication id(geekbyeまたはpavleur)で、backendが正しいプロダクトの挙動へルーティングできます。第二に、クライアントがあらゆるbackend呼び出しに付けるUser-Agentで、Product/version (platform)の形をしています — 例えばPavleur/1.2.5 (win32)やGeekBye/1.7.3 (darwin)です。このヘッダー一つで、backendは別のアナリティクスフィールドなしに、プロダクトごとかつOSごとの区別を無料で手に入れます。一方で自動更新は分離されたままです: 各プロダクトは自分自身のリリースリポジトリに公開するので、GeekByeの更新がPavleurのインストールに配信されることは決してありません。
オンボーディングもプロダクトごとに異なります — GeekByeのウィザードはミーティングの文字起こしについて、Pavleurのウィザードは面接練習について — そして正しい方が、焼き込まれた単一のapplication idからランタイムに選ばれます。同じ仕組み、二つの玄関です。
バグ: Pavleurが自分をGeekByeと名乗ったとき
これが戦記であり、あらゆるホワイトラベルシステムがそのバリエーションを持つものです。
config生成は動いていました。アイコンは入れ替わりました。マイクのプロンプトはブランド付けされていました。プロトコルとバンドルidは正しかった。目に見えるあらゆる尺度で、PavleurビルドはPavleurでした。それなのにしばらくの間、あらゆるPavleurビルドは自分をGeekByeと名乗ってbackendに電話をかけていたのです。
犯人はハードコードされたたった一つの文字列で、想像しうる最も地味な場所に潜んでいました: ネットワークのリトライヘルパーの中に押し込まれた、User-Agentビルダーです。ユーザーの目に触れるものはすべてconfig経由になっていましたが、この一つの深い配管ユーティリティだけは、まだ`GeekBye/${version}`というリテラルを抱えていました。誰もリトライユーティリティを「ブランディング」だとは思わないので、誰もそれを確認していなかったのです。修正は四行 — configから動的なproductNameをインポートし、ハードコードされたブランドの代わりにそれを埋め込む — でしたが、教訓こそが要点です:
ホワイトラベル化とは「ロゴが変わったか」ではない。「すべての識別文字列がconfig経由になっているか」だ。 そして最後の生き残りは、あなたが探すであろうUIには決していない。それはUser-Agent、ディープリンクのプロトコルハンドラ、許可プロンプトの文言、エラーメッセージ — 誰も「ブランド」に分類しない配管に住んでいる。唯一の信頼できる防御は、ブランドリテラルを能動的に狩る対象として扱うことだ: ハードコードされた名前をコードベースでgrepし、それを定義するはずのconfigの外側のどこかにそれが現れたらビルドを失敗させよ。
ホワイトラベル化が教えた三つのこと
- 分岐するな、生成せよ。 二つの小さなconfigモジュールのビルド時コード生成に加えて選択的なアセットコピーは、フォークにもランタイムのプロダクトチェックの茂みにも勝る。アプリはブランド非依存のまま; ブランドはビルドの入力だ。
- 識別文字列は配管に漏れ出す。 UIは簡単な90%だ。難しい10%はUser-Agent、URLプロトコル、マイク許可の文字列、エラーの文言。それらをすべて一つの
productName/applicationIdのエクスポート経由にせよ — そして生のブランド名をCIゲートとしてgrepし、はぐれリテラルが決して出荷されないようにせよ。 - 一つのbackend、多数のブランド、idとヘッダーで区別せよ。 ビルド時のapplication idに
Product/version (platform)のUser-Agentを加えれば、重複したインフラゼロで、プロダクトごと・プラットフォームごとの区別が手に入る — 不変のビルド時アイデンティティを、ランタイムの挙動からきれいに切り離しながら。
自分のブランドでホワイトラベルのノートテイカーを走らせることの、プロダクトとプライバシーの側面については、ホワイトラベルAIノートテイカーをご覧ください。これはGeekBye v2になるv1の物語の第六章です — 前の章については、127コミットのリリースとは実際に何なのか(v1.7.0)を; そして弧の全体については、ソフトウェアを完璧に出荷することの解剖学をご覧ください。