
एक codebase, दो app: बिना fork किए white-label कैसे करें
GeekBye और Pavleur दो अलग-अलग ब्रांड वाले डेस्कटॉप app हैं जो एक ही repository से बनते हैं — न कोई fork, न डुप्लीकेट codebase। यह रही वह build-time मशीनरी जो एक codebase को दो product में कंपाइल करती है, और वह एक-लाइन का बग जिसने हमारे दूसरे app से गलत नाम से अपना परिचय करवा दिया।
GeekBye एक मीटिंग नोटटेकर है। Pavleur एक AI इंटरव्यू-प्रैक्टिस app है। वे दो अलग product जैसे दिखते हैं क्योंकि वे हैं ही दो अलग product — पर वे एक codebase से बनते हैं, बिना किसी fork के और बिना किसी ऐसी branch के जो धीरे-धीरे बहक जाए। यह white-labeling का इंजीनियरिंग पक्ष है: "ब्रांडेड app क्यों बेचें" नहीं (वह एक अलग पोस्ट है), बल्कि एक repository को असल में दो app में कैसे कंपाइल किया जाता है — और वह बेरौनक़ बग जो हर white-label सिस्टम में तब तक छिपा रहता है जब तक आप उसे ढूँढने न निकलें।
Generate करो, branch मत करो
एक codebase से दो product चलाने का ग़लत तरीक़ा है पूरे app में if (product === 'pavleur') छिड़क देना, या fork करके दुआ करना कि दोनों कॉपियाँ कभी अलग न हों। दोनों तेज़ी से सड़ते हैं।
GeekBye का तरीक़ा है build-time code generation। हर product की एक config फ़ाइल है — configs/geekbye/config.ts, configs/pavleur/config.ts — जिसमें उसकी पहचान रहती है: नाम, bundle id, URL protocol, auto-update के लिए releases repository, वग़ैरह। build से पहले एक script चलती है:
node scripts/build-product.js pavleur
वह script चुने हुए product की config पढ़ती है और तीन ठोस काम करती है:
- दो config मॉड्यूल generate करती है — एक renderer के लिए (
src/config/product.ts), एक main process के लिए (electron/config/product.ts) — हर एक पर "DO NOT EDIT MANUALLY — changes will be overwritten" वाला हेडर ठप्पा लगा होता है। बाकी का app बस config सेproductName,applicationIdऔरprotocolimport करता है और कभी नहीं सोचता कि वह कौन-सा ब्रांड है। - सिर्फ़ उसी ब्रांड के assets कॉपी करती है — icon, छोटा logo, toast-notification वाला icon। एक GeekBye build में भौतिक रूप से Pavleur की एक भी आर्ट नहीं होती, और उल्टा भी। लीक होने को कुछ बचता ही नहीं।
package.jsonदोबारा लिखती है (release build पर) — app id, product का नाम, URL protocol, publish करने वाला GitHub releases repo, और, बड़े सलीक़े से, ब्रांडेड माइक्रोफ़ोन-अनुमति टेक्स्ट भी, ताकि macOS पूछे "Pavleur माइक्रोफ़ोन इस्तेमाल करना चाहता है," न कि ग़लत ब्रांड।
पूरा app ब्रांड से अनजान है; ब्रांड build के समय इंजेक्ट होता है। runtime पर ग़लत होने के लिए कोई product-स्विच है ही नहीं, क्योंकि runtime पर हमेशा सिर्फ़ एक product ही build में शामिल होता है।
एक backend, दो ब्रांड
दोनों app एक ही backend से बात करते हैं। तो सर्वर को कैसे पता चलता है कि कोई request किस product की है — और किस pricing, prompts और limits की?
दो संकेत, दोनों build के समय तय। पहला, एक application id (geekbye या pavleur) जो build के समय ही config में शामिल हो जाता है और requests के साथ भेजा जाता है, ताकि backend सही product के व्यवहार तक route कर सके। दूसरा, एक User-Agent जिसे client हर backend कॉल के साथ जोड़ता है, Product/version (platform) के आकार में — जैसे Pavleur/1.2.5 (win32) या GeekBye/1.7.3 (darwin)। वह एक हेडर backend को per-product और per-OS एट्रिब्यूशन मुफ़्त में दे देता है, बिना किसी अलग analytics फ़ील्ड के। इस बीच auto-update अलग-थलग रहते हैं: हर product अपनी releases repository पर publish करता है, इसलिए GeekBye का अपडेट कभी किसी Pavleur इंस्टॉल को सर्व नहीं हो सकता।
onboarding भी हर product का अलग है — GeekBye का विज़ार्ड मीटिंग ट्रांसक्रिप्शन के बारे में है, Pavleur का इंटरव्यू प्रैक्टिस के बारे में — और सही वाला runtime पर उसी build में शामिल application id से चुना जाता है। मशीनरी एक, दरवाज़े दो।
वह बग: जब Pavleur ने खुद को GeekBye कहा
यह रही युद्ध-कथा, और हर white-label सिस्टम के पास इसका कोई-न-कोई रूप होता है।
config generation चल रहा था। icon बदल रहे थे। mic का प्रॉम्प्ट ब्रांडेड था। protocol और bundle id सही थे। हर दिखने वाले पैमाने पर एक Pavleur build था ही Pavleur। और फिर भी, कुछ समय तक, हर Pavleur build backend को फ़ोन करके खुद को GeekBye बताता रहा।
अपराधी एक अकेली hardcode स्ट्रिंग थी, और वह कल्पना की जा सकने वाली सबसे बेरौनक़ जगह में छिपी थी: User-Agent builder, जो एक नेटवर्क retry helper के भीतर दबा हुआ था। उपयोगकर्ता को दिखने वाली हर चीज़ config से होकर गुज़ारी जा चुकी थी, पर plumbing में गहरे बैठी इस एक यूटिलिटी में अब भी एक literal `GeekBye/${version}` पड़ा था। retry utility को कोई "ब्रांडिंग" नहीं मानता, इसलिए किसी ने उसे जाँचा ही नहीं। फ़िक्स चार लाइनों का था — config से डायनामिक productName import करो और hardcode ब्रांड की जगह उसे interpolate करो — पर सबक ही पूरा मक़सद है:
white-labeling यह नहीं है कि "logo बदला या नहीं।" यह है कि "क्या हर identity स्ट्रिंग config से होकर गुज़रती है।" और आख़िरी बचे हुए कभी UI में नहीं रहते, जहाँ आप देखेंगे। वे रहते हैं User-Agent में, deep-link protocol handler में, अनुमति-प्रॉम्प्ट की भाषा में, error संदेशों में — वह plumbing जिसे कोई "ब्रांड" की फ़ाइल में नहीं रखता। इकलौता भरोसेमंद बचाव है ब्रांड literal को ऐसी चीज़ मानना जिसका आप सक्रिय रूप से शिकार करते हैं: पूरे codebase में hardcode नाम को grep करो और build fail कर दो अगर वह उस config के बाहर कहीं भी दिखे जिसे उसे परिभाषित करना चाहिए।
तीन बातें जो white-labeling ने हमें सिखाईं
- Generate करो, branch मत करो। दो छोटे config मॉड्यूल का build-time codegen और चुनिंदा asset कॉपी — यह fork और runtime पर product-जाँचों की झाड़ी, दोनों से बेहतर है। app ब्रांड से अनजान रहता है; ब्रांड build का एक इनपुट है।
- identity स्ट्रिंग plumbing में रिसती हैं। UI आसान 90% है। कठिन 10% है User-Agent, URL protocol, mic-अनुमति स्ट्रिंग, error की भाषा। सबको एक ही
productName/applicationIdexport से गुज़ारो — और CI गेट के तौर पर कच्चे ब्रांड नाम को grep करो ताकि कोई भटका हुआ literal कभी ship न हो सके। - एक backend, कई ब्रांड — id और हेडर से पहचान। build-time application id और
Product/version (platform)User-Agent, शून्य डुप्लीकेट इन्फ्रास्ट्रक्चर के साथ per-product और per-platform एट्रिब्यूशन दे देते हैं — अपरिवर्तनीय build-time पहचान को runtime व्यवहार से साफ़-साफ़ अलग करते हुए।
अपने ब्रांड के नीचे white-label नोटटेकर चलाने के product-और-privacy पक्ष के लिए देखें white-label AI नोटटेकर। यह उस v1 कहानी का छठा अध्याय है जो GeekBye v2 बनती है — पिछले अध्याय के लिए, 127-कमिट वाली रिलीज़ असल में क्या होती है (v1.7.0); और पूरे चाप के लिए, सॉफ़्टवेयर को परिपूर्णता तक पहुँचाकर भेजने की शारीर-रचना।