
Satu Codebase, Dua Aplikasi: Cara White-Label Tanpa Fork
GeekBye dan Pavleur adalah dua aplikasi desktop dengan brand berbeda yang dibangun dari satu repository — tanpa fork, tanpa codebase ganda. Inilah mesin build-time yang membuat satu codebase terkompilasi menjadi dua produk, dan bug satu baris yang membuat aplikasi kedua kami memperkenalkan diri dengan nama yang salah.
GeekBye adalah aplikasi pencatat rapat. Pavleur adalah aplikasi latihan wawancara berbasis AI. Keduanya terlihat seperti dua produk berbeda karena memang benar-benar dua produk berbeda — tetapi keduanya dibangun dari satu codebase, tanpa fork dan tanpa branch yang perlahan menyimpang. Ini sisi engineering dari white-labeling: bukan "kenapa menjual aplikasi ber-brand" (itu tulisan lain), melainkan bagaimana kamu benar-benar mengompilasi satu repository menjadi dua aplikasi — dan bug tak glamor yang bersembunyi di setiap sistem white-label sampai kamu sengaja mencarinya.
Generate, bukan branch
Cara yang salah menjalankan dua produk dari satu codebase adalah menaburkan if (product === 'pavleur') di seluruh aplikasi, atau melakukan fork lalu berdoa agar kedua salinan tidak pernah menyimpang. Dua-duanya cepat membusuk.
Pendekatan GeekBye adalah code generation saat build. Setiap produk punya file config — configs/geekbye/config.ts, configs/pavleur/config.ts — yang menyimpan identitasnya: nama, bundle id, protokol URL, repository releases untuk auto-update, dan seterusnya. Sebelum build, satu script dijalankan:
node scripts/build-product.js pavleur
Script itu membaca config produk yang dipilih dan melakukan tiga hal konkret:
- Menghasilkan dua modul config — satu untuk renderer (
src/config/product.ts), satu untuk main process (electron/config/product.ts) — masing-masing dicap dengan header "DO NOT EDIT MANUALLY — changes will be overwritten". Sisa aplikasi tinggal meng-importproductName,applicationId, danprotocoldari config dan tidak pernah memikirkan brand mana dirinya. - Menyalin hanya aset brand itu — ikon, logo kecil, ikon notifikasi toast. Build GeekBye secara fisik berisi nol gambar Pavleur, dan sebaliknya. Tidak ada yang bisa bocor.
- Menulis ulang
package.json(pada build rilis) — app id, nama produk, protokol URL, repo GitHub releases tujuan publish, dan, yang manis, teks izin mikrofon yang ber-brand sehingga macOS bertanya "Pavleur ingin mengakses mikrofon," bukan brand yang salah.
Seluruh aplikasi bersifat brand-agnostic; brand disuntikkan saat build. Tidak ada saklar produk runtime yang bisa salah, karena saat runtime hanya pernah ada satu produk yang tertanam.
Satu backend, dua brand
Kedua aplikasi bicara ke backend yang sama. Lalu bagaimana server tahu sebuah request milik produk yang mana — dan pricing, prompt, serta limit yang mana?
Dua sinyal, keduanya diset saat build. Pertama, sebuah application id (geekbye atau pavleur) yang tertanam di config dan dikirim bersama request, sehingga backend bisa mengarahkan ke perilaku produk yang tepat. Kedua, sebuah User-Agent yang dilampirkan client ke setiap panggilan backend, berbentuk Product/version (platform) — mis. Pavleur/1.2.5 (win32) atau GeekBye/1.7.3 (darwin). Satu header itu memberi backend atribusi per produk dan per OS secara gratis, tanpa field analitik terpisah. Sementara itu, auto-update tetap terisolasi: tiap produk publish ke repository releases-nya sendiri, jadi update GeekBye tidak akan pernah tersaji ke instalasi Pavleur.
Onboarding-nya pun berbeda per produk — wizard GeekBye soal transkripsi rapat, wizard Pavleur soal latihan wawancara — dan yang tepat dipilih saat runtime dari satu application id yang tertanam itu. Mesin yang sama, dua pintu depan.
Bug-nya: saat Pavleur menyebut dirinya GeekBye
Inilah kisah perangnya, dan ini kisah yang versinya dimiliki setiap sistem white-label.
Pembuatan config berjalan. Ikon bertukar. Prompt mikrofon ber-brand. Protokol dan bundle id benar. Dari semua ukuran yang terlihat, build Pavleur adalah Pavleur. Namun untuk beberapa waktu, setiap build Pavleur menghubungi backend sambil menyebut dirinya GeekBye.
Biang keladinya satu string yang di-hardcode, dan ia bersembunyi di tempat paling tak glamor yang bisa dibayangkan: pembangun User-Agent, terselip di dalam helper retry jaringan. Semua yang menghadap pengguna sudah dialihkan lewat config, tapi utilitas perpipaan-dalam yang satu ini masih punya literal `GeekBye/${version}` di dalamnya. Tidak ada yang menganggap utilitas retry sebagai "branding", jadi tidak ada yang memeriksanya. Perbaikannya empat baris — import productName dinamis dari config dan interpolasikan itu alih-alih brand yang di-hardcode — tapi pelajarannya adalah intinya:
White-labeling bukan "apakah logonya berganti." Melainkan "apakah setiap string identitas lewat config." Dan sisa-sisa terakhir tidak pernah tinggal di UI, tempat kamu akan mencarinya. Mereka tinggal di User-Agent, handler protokol deep-link, teks prompt izin, pesan error — perpipaan yang tak seorang pun mengarsipkannya di bawah label "brand". Satu-satunya pertahanan yang andal adalah memperlakukan literal brand sebagai sesuatu yang kamu buru secara aktif: grep codebase untuk nama yang di-hardcode dan gagalkan build kalau ia muncul di mana pun di luar config yang memang seharusnya mendefinisikannya.
Tiga hal yang diajarkan white-labeling kepada kami
- Generate, bukan branch. Codegen saat build untuk dua modul config kecil plus penyalinan aset selektif mengalahkan fork maupun semak belukar pengecekan produk saat runtime. Aplikasi tetap brand-agnostic; brand adalah input build.
- String identitas bocor ke perpipaan. UI adalah 90% yang mudah. 10% yang sulit adalah User-Agent, protokol URL, string izin mikrofon, teks error. Alirkan semuanya lewat satu export
productName/applicationId— dan grep nama brand mentah sebagai gerbang CI supaya literal nyasar tidak akan pernah ikut rilis. - Satu backend, banyak brand, bedakan lewat id dan header. Application id saat build plus User-Agent
Product/version (platform)memberi atribusi per produk dan per platform tanpa infrastruktur ganda — memisahkan dengan bersih identitas build-time yang tak berubah dari perilaku runtime.
Untuk sisi produk-dan-privasi menjalankan notetaker white-label di bawah brand-mu sendiri, lihat notetaker AI white-label. Ini bab keenam dari kisah v1 yang menjadi GeekBye v2 — untuk bab sebelumnya, apa sebenarnya rilis 127 commit itu (v1.7.0); dan untuk keseluruhan busurnya, anatomi merilis perangkat lunak hingga kesempurnaan.