
Én kodebase, to apper: hvordan white-labele uten å forke
GeekBye og Pavleur er to ulikt brandede desktop-apper bygget fra ett repository — ingen fork, ingen duplisert kodebase. Her er build-tids-maskineriet som får én kodebase til å kompilere til to produkter, og én-linjes-feilen som fikk vår andre app til å presentere seg med feil navn.
GeekBye er en møtenotattaker. Pavleur er en AI-app for intervjutrening. De ser ut som to forskjellige produkter fordi de er to forskjellige produkter — men de bygges fra én kodebase, uten fork og uten en branch som drifter av gårde. Dette er ingeniørsiden av white-labeling: ikke "hvorfor selge en brandet app" (det er et annet innlegg), men hvordan du faktisk kompilerer ett repository til to apper — og den uglamorøse feilen som gjemmer seg i ethvert white-label-system til du går på jakt etter den.
Generer, ikke branch
Feil måte å kjøre to produkter fra én kodebase på er å strø if (product === 'pavleur') utover hele appen, eller å forke og be om at de to kopiene aldri divergerer. Begge deler råtner fort.
GeekByes tilnærming er kodegenerering på build-tid. Hvert produkt har en konfigurasjonsfil — configs/geekbye/config.ts, configs/pavleur/config.ts — som holder identiteten dets: navn, bundle id, URL-protokoll, releases-repositoriet for auto-oppdateringer, og så videre. Før et build kjøres ett skript:
node scripts/build-product.js pavleur
Skriptet leser det valgte produktets konfigurasjon og gjør tre konkrete ting:
- Genererer to konfigurasjonsmoduler — én for rendereren (
src/config/product.ts), én for main-prosessen (electron/config/product.ts) — begge stemplet med en "DO NOT EDIT MANUALLY — changes will be overwritten"-header. Resten av appen importerer bareproductName,applicationIdogprotocolfra config og tenker aldri på hvilket brand den er. - Kopierer kun det brandets assets — ikoner, den lille logoen, toast-varselikonet. Et GeekBye-build inneholder fysisk null Pavleur-grafikk og omvendt. Ingenting å lekke.
- Skriver om
package.json(på release-builds) — app-id, produktnavn, URL-protokoll, GitHub-releases-repoet det publiseres til, og, som en fin detalj, den brandede mikrofontillatelsesteksten, slik at macOS spør "Pavleur ønsker tilgang til mikrofonen", ikke feil brand.
Hele appen er brand-agnostisk; brandet injiseres på build-tid. Det finnes ingen runtime-produktbryter å rote til, for på runtime er det alltid bare bakt inn ett produkt.
Én backend, to brands
Begge appene snakker med samme backend. Så hvordan vet serveren hvilket produkt — og hvilken prising, hvilke prompts og hvilke grenser — en request hører til?
To signaler, begge satt på build-tid. Først en application id (geekbye eller pavleur) bakt inn i konfigurasjonen og sendt med requests, slik at backend kan rute til riktig produkts oppførsel. Deretter en User-Agent som klienten fester på hvert backend-kall, på formen Product/version (platform) — f.eks. Pavleur/1.2.5 (win32) eller GeekBye/1.7.3 (darwin). Den ene headeren gir backend attribusjon per produkt og per OS gratis, uten et eget analytics-felt. Auto-oppdateringer holder seg imens isolert: hvert produkt publiserer til sitt eget releases-repository, så en GeekBye-oppdatering kan aldri serveres til en Pavleur-installasjon.
Onboardingen er også forskjellig per produkt — GeekByes veiviser handler om møtetranskripsjon, Pavleurs om intervjutrening — og den riktige velges på runtime fra den ene innbakte application id-en. Samme maskineri, to inngangsdører.
Feilen: da Pavleur kalte seg selv GeekBye
Her er krigshistorien, og det er den ethvert white-label-system har en versjon av.
Konfigurasjonsgenereringen virket. Ikonene byttet. Mikrofonprompten var brandet. Protokollene og bundle id-ene var riktige. Etter alle synlige mål var et Pavleur-build Pavleur. Og likevel ringte hvert Pavleur-build i en periode hjem til backend og kalte seg selv GeekBye.
Synderen var én enkelt hardkodet streng, og den gjemte seg på det minst glamorøse stedet man kan tenke seg: i User-Agent-byggeren, stappet inn i en nettverks-retry-helper. Alt brukervendt var rutet gjennom config, men denne ene dypt begravde utility-en hadde fortsatt en literal `GeekBye/${version}` i seg. Ingen tenker på en retry-utility som "branding", så ingen hadde sjekket den. Fiksen var fire linjer — importer det dynamiske productName fra config og interpoler det i stedet for det hardkodede brandet — men lærdommen er hele poenget:
White-labeling er ikke "byttet logoen seg". Det er "går hver eneste identitetsstreng gjennom config." Og de siste gjenværende bor aldri i UI-et, der du ville lett. De bor i User-Agenten, deep-link-protokollhandleren, tillatelsesprompt-tekstene, feilmeldingene — rørleggerarbeidet ingen arkiverer under "brand". Det eneste pålitelige forsvaret er å behandle brand-literalen som noe du aktivt jakter på: grep kodebasen for det hardkodede navnet og la buildet feile hvis det dukker opp noe sted utenfor konfigurasjonen som er ment å definere det.
Tre ting white-labeling lærte oss
- Generer, ikke branch. Build-tids-codegen av to bittesmå konfigurasjonsmoduler pluss selektiv asset-kopiering slår både forking og et runtime-kratt av produktsjekker. Appen forblir brand-agnostisk; brandet er et build-input.
- Identitetsstrenger lekker ut i rørleggerarbeidet. UI-et er de enkle 90 %. De vanskelige 10 % er User-Agenten, URL-protokollen, mikrofontillatelsesstrengen, feiltekstene. Rut dem alle gjennom én
productName- /applicationId-export — og grep etter det rå brandnavnet som en CI-gate, slik at en løsreven literal aldri kan shippe. - Én backend, mange brands, disambiguer med id og header. En build-tids application id pluss en
Product/version (platform)User-Agent gir attribusjon per produkt og per plattform med null duplisert infrastruktur — og skiller rent den uforanderlige build-tids-identiteten fra runtime-oppførselen.
For produkt- og personvernsiden av å kjøre en white-label-notattaker under ditt eget brand, se white-label AI-notattakeren. Dette er sjette kapittel i v1-historien som blir til GeekBye v2 — for det forrige kapittelet, hva en release på 127 commits egentlig er (v1.7.0); og for hele buen, anatomien til å levere programvare til perfeksjon.