Steven
Steven6 min di lettura

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.

Ingegneria
Architettura
Build
Release di GeekBye
Una codebase, due app: come fare white-label senza fork

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:

  1. 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 importare productName, applicationId e protocol dalla config e non pensa mai a quale brand sia.
  2. 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.
  3. 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

  1. 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.
  2. 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.
  3. 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.

Articoli Correlati

Una variabile CSS, cinque round di revisione e una toolchain Swift che ha mentito
Steven
Steven8 min di lettura

Una variabile CSS, cinque round di revisione e una toolchain Swift che ha mentito

La v2.0.7 di GeekBye ha reso l'intero overlay traslucido in modo regolabile — cosa che suona come una modifica CSS di una riga e non lo era affatto. La storia vera è ciò che ha intercettato la revisione del codice: due superfici che si rifiutavano di sfumare, una barra di registrazione che sembrava sbagliata alla stessa opacità e un binario compilato che oscillava di 672 byte perché la nostra stessa documentazione ha indicato a un revisore la versione sbagliata di Swift.

Ingegneria
Design
Build
Abbiamo cancellato 5,000 righe di codice audio — e le trascrizioni hanno iniziato ad apparire due volte
Steven
Steven6 min di lettura

Abbiamo cancellato 5,000 righe di codice audio — e le trascrizioni hanno iniziato ad apparire due volte

GeekBye v1.6 ha eliminato due trascrittori Swift on-device e li ha sostituiti con un'unica pipeline — una riduzione netta di oltre 5,000 righe, eseguita mentre le persone erano a metà riunione sull'app. Poi le trascrizioni hanno iniziato ad apparire due volte, perché il bug si è spostato nell'unico livello che credeva ancora di avere due motori.

Ingegneria
Trascrizione
Architettura
Cos'è davvero una release da 127 commit
Steven
Steven6 min di lettura

Cos'è davvero una release da 127 commit

GeekBye v1.7.0 sono stati 127 commit in undici giorni. Da fuori sembrano cento piccole cose. Da dentro erano due grandi funzionalità intrecciate — e una è stata costruita nel posto sbagliato, poi strappata via e ricostruita a metà release. Ecco l'anatomia di una release grande.

Ingegneria
Release
Architettura