
Una codebase, due app: come fare white-label senza fork
GeekBye e Pavleur sono due app desktop con brand diversi costruite da un unico repository — nessun fork, nessuna codebase duplicata. Ecco la macchina di build che fa compilare una sola codebase in due prodotti, e il bug da una riga che ha fatto presentare la nostra seconda app con il nome sbagliato.
GeekBye è un notetaker per riunioni. Pavleur è un'app di pratica per colloqui con IA. Sembrano due prodotti diversi perché sono due prodotti diversi — ma sono costruiti da una sola codebase, senza fork e senza branch che diverge nel tempo. Questo è il lato ingegneristico del white-labeling: non "perché vendere un'app brandizzata" (quello è un altro articolo), ma come si compila davvero un unico repository in due app — e il bug poco glamour che si nasconde in ogni sistema white-label finché non lo vai a cercare.
Genera, non fare branch
Il modo sbagliato di far girare due prodotti da una codebase è spargere if (product === 'pavleur') per tutta l'app, oppure fare un fork e pregare che le due copie non divergano mai. Entrambi marciscono in fretta.
L'approccio di GeekBye è la generazione di codice a build time. Ogni prodotto ha un file di configurazione — configs/geekbye/config.ts, configs/pavleur/config.ts — che ne contiene l'identità: nome, bundle id, protocollo URL, il repository delle release per gli aggiornamenti automatici, e così via. Prima di una build, gira un solo script:
node scripts/build-product.js pavleur
Quello script legge la config del prodotto scelto e fa tre cose concrete:
- Genera due moduli di configurazione — uno per il renderer (
src/config/product.ts), uno per il processo main (electron/config/product.ts) — ognuno marchiato con un header "DO NOT EDIT MANUALLY — changes will be overwritten". Il resto dell'app si limita a importareproductName,applicationIdeprotocoldalla config e non pensa mai a quale brand sia. - Copia solo gli asset di quel brand — le icone, il logo piccolo, l'icona delle notifiche toast. Una build di GeekBye contiene fisicamente zero grafica di Pavleur e viceversa. Niente da far trapelare.
- Riscrive
package.json(nelle build di release) — l'app id, il nome del prodotto, il protocollo URL, il repo GitHub delle release su cui pubblicare e, cosa gradita, il testo brandizzato del permesso microfono, così macOS chiede "Pavleur vorrebbe accedere al microfono", non il brand sbagliato.
L'intera app è agnostica rispetto al brand; il brand viene iniettato a build time. Non c'è nessuno switch di prodotto a runtime da sbagliare, perché a runtime esiste sempre e solo un prodotto cotto dentro.
Un backend, due brand
Entrambe le app parlano con lo stesso backend. Quindi come fa il server a sapere a quale prodotto — e a quali prezzi, prompt e limiti — appartiene una richiesta?
Due segnali, entrambi impostati a build time. Primo, un application id (geekbye o pavleur) cotto nella config e inviato con le richieste, così il backend può instradare verso il comportamento del prodotto giusto. Secondo, uno User-Agent che il client allega a ogni chiamata al backend, della forma Product/version (platform) — per esempio Pavleur/1.2.5 (win32) o GeekBye/1.7.3 (darwin). Quell'unico header regala al backend l'attribuzione per prodotto e per sistema operativo, senza un campo di analytics separato. Gli aggiornamenti automatici, intanto, restano isolati: ogni prodotto pubblica sul proprio repository di release, quindi un aggiornamento di GeekBye non potrà mai essere servito a un'installazione di Pavleur.
Anche l'onboarding differisce per prodotto — il wizard di GeekBye parla di trascrizione delle riunioni, quello di Pavleur di pratica per i colloqui — e quello giusto viene scelto a runtime a partire dall'unico application id cotto dentro. Stessa macchina, due porte d'ingresso.
Il bug: quando Pavleur si è presentato come GeekBye
Ecco la storia di guerra, ed è quella di cui ogni sistema white-label ha una propria versione.
La generazione della config funzionava. Le icone si scambiavano. Il prompt del microfono era brandizzato. Protocolli e bundle id erano giusti. Sotto ogni aspetto visibile, una build di Pavleur era Pavleur. Eppure, per un po', ogni build di Pavleur telefonava a casa al backend presentandosi come GeekBye.
Il colpevole era una singola stringa hardcoded, nascosta nel posto meno glamour immaginabile: il costruttore dello User-Agent, infilato dentro un helper di retry di rete. Tutto ciò che era rivolto all'utente era stato instradato attraverso la config, ma questa utility di idraulica profonda aveva ancora dentro un literal `GeekBye/${version}`. Nessuno pensa a una utility di retry come a "branding", quindi nessuno l'aveva controllata. Il fix erano quattro righe — importare il productName dinamico dalla config e interpolarlo al posto del brand hardcoded — ma la lezione è tutto il punto:
Il white-labeling non è "il logo è cambiato?". È "ogni stringa di identità passa dalla config?" E gli ultimi irriducibili non vivono mai nella UI, dove andresti a guardare. Vivono nello User-Agent, nel gestore del protocollo dei deep link, nei testi dei prompt di permesso, nei messaggi di errore — l'idraulica che nessuno archivia sotto "brand". L'unica difesa affidabile è trattare il literal del brand come qualcosa da cacciare attivamente: fare grep del nome hardcoded su tutta la codebase e far fallire la build se compare ovunque fuori dalla config che dovrebbe definirlo.
Tre cose che il white-labeling ci ha insegnato
- Genera, non fare branch. La codegen a build time di due piccoli moduli di configurazione più la copia selettiva degli asset batte sia il fork sia una boscaglia di controlli di prodotto a runtime. L'app resta agnostica rispetto al brand; il brand è un input di build.
- Le stringhe di identità colano nell'idraulica. La UI è il 90% facile. Il 10% difficile è lo User-Agent, il protocollo URL, la stringa del permesso microfono, i testi di errore. Falli passare tutti da un unico export
productName/applicationId— e fai grep del nome del brand grezzo come gate di CI, così un literal vagante non potrà mai andare in produzione. - Un backend, molti brand, distinti da id e header. Un application id a build time più uno User-Agent
Product/version (platform)danno attribuzione per prodotto e per piattaforma con zero infrastruttura duplicata — separando in modo pulito l'identità immutabile di build time dal comportamento a runtime.
Per il lato prodotto-e-privacy di far girare un notetaker white-label con il tuo brand, vedi il notetaker IA white-label. Questo è il sesto capitolo della storia della v1 che diventa GeekBye v2 — per il capitolo precedente, cos'è davvero una release da 127 commit (v1.7.0); e per tutto l'arco, l'anatomia del pubblicare software fino alla perfezione.