Steven
Steven5 min læsning

Én kodebase, to apps: sådan white-labeler du uden at forke

GeekBye og Pavleur er to forskelligt brandede desktop-apps bygget fra ét repository — ingen fork, ingen duplikeret kodebase. Her er build-tids-maskineriet, der får én kodebase til at kompilere til to produkter, og den én-linjes fejl, der fik vores anden app til at præsentere sig med det forkerte navn.

Udvikling
Arkitektur
Build
GeekBye-releaser
Én kodebase, to apps: sådan white-labeler du uden at forke

GeekBye er en mødenotetager. Pavleur er en AI-app til interviewtræning. De ligner to forskellige produkter, fordi de er to forskellige produkter — men de bygges fra én kodebase, uden fork og uden en branch, der driver væk. Dette er ingeniørsiden af white-labeling: ikke "hvorfor sælge en brandet app" (det er et andet indlæg), men hvordan man faktisk kompilerer ét repository til to apps — og den uglamourøse fejl, der gemmer sig i ethvert white-label-system, indtil man går på jagt efter den.

Generér, lad være med at branche

Den forkerte måde at køre to produkter fra én kodebase på er at drysse if (product === 'pavleur') ud over hele appen, eller at forke og bede til, at de to kopier aldrig divergerer. Begge dele rådner hurtigt.

GeekByes tilgang er kodegenerering på build-tid. Hvert produkt har en konfigurationsfil — configs/geekbye/config.ts, configs/pavleur/config.ts — der rummer dets identitet: navn, bundle id, URL-protokol, releases-repositoriet til auto-opdateringer og så videre. Før et build kører ét script:

node scripts/build-product.js pavleur

Scriptet læser det valgte produkts konfiguration og gør tre konkrete ting:

  1. Genererer to konfigurationsmoduler — ét til rendereren (src/config/product.ts), ét til main-processen (electron/config/product.ts) — begge stemplet med en "DO NOT EDIT MANUALLY — changes will be overwritten"-header. Resten af appen importerer bare productName, applicationId og protocol fra config og tænker aldrig over, hvilket brand den er.
  2. Kopierer kun det brands assets — ikoner, det lille logo, toast-notifikationsikonet. Et GeekBye-build indeholder fysisk nul Pavleur-grafik og omvendt. Intet at lække.
  3. Omskriver package.json (på release-builds) — app-id, produktnavn, URL-protokol, det GitHub-releases-repo der publiceres til, og, som en fin detalje, den brandede mikrofontilladelsestekst, så macOS spørger "Pavleur vil gerne have adgang til mikrofonen" og ikke det forkerte brand.

Hele appen er brand-agnostisk; brandet injiceres på build-tid. Der er ingen runtime-produktswitch at få galt i halsen, for på runtime er der altid kun bagt ét produkt ind.

Én backend, to brands

Begge apps taler med den samme backend. Så hvordan ved serveren, hvilket produkt — og hvilken prissætning, hvilke prompts og hvilke grænser — en request hører til?

To signaler, begge sat på build-tid. Først et application id (geekbye eller pavleur), bagt ind i konfigurationen og sendt med requests, så backenden kan route til det rigtige produkts adfærd. Dernæst en User-Agent, som klienten hæfter på hvert backend-kald, på formen Product/version (platform) — f.eks. Pavleur/1.2.5 (win32) eller GeekBye/1.7.3 (darwin). Den ene header giver backenden attribution pr. produkt og pr. OS gratis, uden et separat analytics-felt. Auto-opdateringer forbliver imens isolerede: hvert produkt publicerer til sit eget releases-repository, så en GeekBye-opdatering aldrig kan serveres til en Pavleur-installation.

Onboardingen er også forskellig pr. produkt — GeekByes wizard handler om mødetransskription, Pavleurs om interviewtræning — og den rigtige vælges på runtime ud fra det ene indbagte application id. Samme maskineri, to hoveddøre.

Fejlen: da Pavleur kaldte sig selv GeekBye

Her er krigshistorien, og det er den, ethvert white-label-system har en version af.

Konfigurationsgenereringen virkede. Ikonerne skiftede. Mikrofonprompten var brandet. Protokollerne og bundle id'erne var rigtige. På alle synlige mål var et Pavleur-build Pavleur. Og alligevel ringede hvert Pavleur-build i en periode hjem til backenden og kaldte sig selv GeekBye.

Synderen var en enkelt hardcodet streng, og den gemte sig det mindst glamourøse sted overhovedet: i User-Agent-byggeren, gemt inde i en netværks-retry-helper. Alt brugervendt var routet gennem config, men denne ene dybt begravede utility havde stadig en literal `GeekBye/${version}` i sig. Ingen tænker på en retry-utility som "branding", så ingen havde tjekket den. Rettelsen var fire linjer — importér det dynamiske productName fra config og interpolér det i stedet for det hardcodede brand — men læren er hele pointen:

White-labeling er ikke "skiftede logoet". Det er "går hver eneste identitetsstreng gennem config." Og de sidste efternølere bor aldrig i UI'et, hvor man ville kigge. De bor i User-Agenten, deep-link-protokolhandleren, tilladelsesprompt-teksterne, fejlbeskederne — de rør, ingen arkiverer under "brand". Det eneste pålidelige forsvar er at behandle brand-literalen som noget, man aktivt jagter: grep kodebasen for det hardcodede navn, og lad buildet fejle, hvis det dukker op noget sted uden for den config, der skal definere det.

Tre ting white-labeling lærte os

  1. Generér, lad være med at branche. Build-tids-codegen af to bittesmå konfigurationsmoduler plus selektiv asset-kopiering slår både forking og et runtime-krat af produkttjek. Appen forbliver brand-agnostisk; brandet er et build-input.
  2. Identitetsstrenge lækker ud i rørene. UI'et er de nemme 90 %. De svære 10 % er User-Agenten, URL-protokollen, mikrofontilladelsesstrengen, fejlteksterne. Rout dem alle gennem én productName- / applicationId-export — og grep efter det rå brandnavn som en CI-gate, så en herreløs literal aldrig kan shippe.
  3. Én backend, mange brands, disambiguér med id og header. Et build-tids application id plus en Product/version (platform) User-Agent giver attribution pr. produkt og pr. platform med nul duplikeret infrastruktur — og adskiller rent den uforanderlige build-tids-identitet fra runtime-adfærden.

For produkt- og privatlivssiden af at køre en white-label-notetager under dit eget brand, se white-label AI-notetageren. Dette er sjette kapitel i v1-historien, der bliver til GeekBye v2 — for det forrige kapitel, hvad en release på 127 commits egentlig er (v1.7.0); og for hele buen, anatomien af at skibbe software til perfektion.