
Một codebase, hai ứng dụng: cách white-label mà không cần fork
GeekBye và Pavleur là hai ứng dụng desktop mang thương hiệu khác nhau được build từ một repository duy nhất — không fork, không nhân đôi codebase. Đây là bộ máy build-time giúp một codebase biên dịch ra hai sản phẩm, và cái bug một dòng khiến ứng dụng thứ hai của chúng tôi tự giới thiệu bằng cái tên sai.
GeekBye là một ứng dụng ghi chú cuộc họp. Pavleur là một ứng dụng luyện phỏng vấn bằng AI. Chúng trông như hai sản phẩm khác nhau bởi chúng là hai sản phẩm khác nhau — nhưng được build từ một codebase, không fork và không có nhánh nào trôi dạt. Đây là mặt kỹ thuật của white-label: không phải "vì sao nên bán một ứng dụng mang thương hiệu riêng" (đó là một bài khác), mà là làm thế nào bạn thực sự biên dịch một repository thành hai ứng dụng — và cái bug kém hào nhoáng ẩn trong mọi hệ thống white-label cho tới khi bạn chủ động đi tìm nó.
Sinh ra, đừng rẽ nhánh
Cách sai để chạy hai sản phẩm từ một codebase là rắc if (product === 'pavleur') khắp ứng dụng, hoặc fork rồi cầu nguyện hai bản sao không bao giờ tách xa nhau. Cả hai đều mục nát rất nhanh.
Cách tiếp cận của GeekBye là sinh mã lúc build. Mỗi sản phẩm có một file config — configs/geekbye/config.ts, configs/pavleur/config.ts — chứa danh tính của nó: tên, bundle id, URL protocol, repository chứa các bản phát hành cho auto-update, v.v. Trước mỗi lần build, một script chạy:
node scripts/build-product.js pavleur
Script đó đọc config của sản phẩm được chọn và làm ba việc cụ thể:
- Sinh ra hai module config — một cho renderer (
src/config/product.ts), một cho main process (electron/config/product.ts) — mỗi cái được đóng dấu header "DO NOT EDIT MANUALLY — changes will be overwritten". Phần còn lại của ứng dụng chỉ việc importproductName,applicationIdvàprotocoltừ config và không bao giờ phải bận tâm mình là thương hiệu nào. - Chỉ copy assets của thương hiệu đó — các icon, logo nhỏ, icon thông báo toast. Một bản build GeekBye về mặt vật lý không chứa một mẩu đồ họa Pavleur nào và ngược lại. Không có gì để rò rỉ.
- Ghi lại
package.json(ở các bản build phát hành) — app id, tên sản phẩm, URL protocol, repo GitHub releases để publish lên, và, rất gọn gàng, cả đoạn văn bản xin quyền microphone mang thương hiệu để macOS hỏi "Pavleur muốn truy cập microphone," chứ không phải nhầm thương hiệu.
Toàn bộ ứng dụng không biết gì về thương hiệu; thương hiệu được tiêm vào lúc build. Không có công tắc chuyển sản phẩm lúc runtime để mà làm sai, bởi lúc runtime chỉ có duy nhất một sản phẩm đã được nướng sẵn vào.
Một backend, hai thương hiệu
Cả hai ứng dụng nói chuyện với cùng một backend. Vậy làm sao máy chủ biết một request thuộc về sản phẩm nào — với bảng giá, prompt và giới hạn nào?
Hai tín hiệu, cả hai đều được gắn lúc build. Thứ nhất, một application id (geekbye hoặc pavleur) được nướng vào config và gửi kèm các request, để backend định tuyến đến hành vi của đúng sản phẩm. Thứ hai, một User-Agent mà client đính vào mọi lệnh gọi backend, theo dạng Product/version (platform) — ví dụ Pavleur/1.2.5 (win32) hay GeekBye/1.7.3 (darwin). Chỉ một header đó đã cho backend khả năng quy nguồn theo sản phẩm và theo hệ điều hành miễn phí, không cần một trường analytics riêng. Trong khi đó, auto-update vẫn được cách ly: mỗi sản phẩm publish lên repository releases của riêng mình, nên một bản cập nhật GeekBye không bao giờ có thể được đưa tới một bản cài Pavleur.
Onboarding cũng khác nhau theo từng sản phẩm — wizard của GeekBye nói về phiên âm cuộc họp, của Pavleur nói về luyện phỏng vấn — và cái đúng được chọn lúc runtime từ chính application id duy nhất đã nướng sẵn. Cùng một bộ máy, hai cửa trước.
Cái bug: khi Pavleur tự gọi mình là GeekBye
Đây là chuyện chiến trường, và mọi hệ thống white-label đều có một phiên bản của nó.
Việc sinh config chạy tốt. Icon đổi đúng. Lời nhắc mic mang đúng thương hiệu. Protocol và bundle id đều chuẩn. Theo mọi thước đo nhìn thấy được, một bản build Pavleur chính là Pavleur. Vậy mà, trong một thời gian, mọi bản build Pavleur đều gọi về backend và tự xưng là GeekBye.
Thủ phạm là một chuỗi hardcode duy nhất, và nó ẩn ở nơi kém hào nhoáng nhất có thể tưởng tượng: bộ dựng User-Agent, nằm gọn trong một retry helper của tầng mạng. Mọi thứ hướng tới người dùng đều đã được dẫn qua config, nhưng cái tiện ích nằm sâu trong đường ống này vẫn còn một literal `GeekBye/${version}` bên trong. Chẳng ai nghĩ một retry utility là "việc thương hiệu," nên chẳng ai kiểm tra nó. Bản sửa chỉ bốn dòng — import productName động từ config và nội suy nó thay cho thương hiệu hardcode — nhưng bài học mới là toàn bộ vấn đề:
White-label không phải là "logo đã đổi chưa." Mà là "mọi chuỗi định danh có đi qua config không." Và những kẻ sót lại cuối cùng không bao giờ nằm trong UI, nơi bạn sẽ nhìn vào. Chúng nằm trong User-Agent, trình xử lý protocol của deep link, câu chữ xin quyền, các thông báo lỗi — thứ đường ống mà không ai xếp vào mục "thương hiệu." Phòng tuyến đáng tin duy nhất là coi literal thương hiệu như một thứ phải chủ động săn lùng: grep cả codebase tìm cái tên hardcode và làm fail bản build nếu nó xuất hiện ở bất kỳ đâu ngoài chỗ config vốn được giao nhiệm vụ định nghĩa nó.
Ba điều white-label dạy chúng tôi
- Sinh ra, đừng rẽ nhánh. Sinh mã lúc build cho hai module config nhỏ cộng với copy assets có chọn lọc thắng cả fork lẫn một rừng kiểm tra product lúc runtime. Ứng dụng giữ nguyên trạng thái không biết gì về thương hiệu; thương hiệu là một đầu vào của build.
- Chuỗi định danh rò rỉ vào đường ống. UI là 90% dễ. 10% khó là User-Agent, URL protocol, chuỗi xin quyền mic, câu chữ báo lỗi. Hãy dẫn tất cả qua một export
productName/applicationIdduy nhất — và grep tên thương hiệu thô như một cổng chặn trong CI để không một literal lạc loài nào có thể lên tàu. - Một backend, nhiều thương hiệu, phân biệt bằng id và header. Một application id lúc build cộng với User-Agent dạng
Product/version (platform)cho khả năng quy nguồn theo sản phẩm và theo nền tảng mà không nhân đôi chút hạ tầng nào — tách bạch gọn gàng danh tính build-time bất biến khỏi hành vi runtime.
Về mặt sản phẩm và quyền riêng tư của việc vận hành một notetaker white-label dưới thương hiệu của riêng bạn, xem AI notetaker white-label. Đây là chương thứ sáu của câu chuyện v1 sẽ trở thành GeekBye v2 — về chương trước, xem một bản phát hành 127 commit thực chất là gì (v1.7.0); và cho toàn bộ vòng cung, xem giải phẫu việc xuất xưởng phần mềm đến độ hoàn hảo.