How do you architect several brands or white-label apps from one codebase: configuration, theming, build variants, release and the risk of forks?

26.3.1 · L8 · Interview

Predict first

The same source is built with -DBRAND_A, with -DBRAND_B and with no flag. What do the three binaries print?

// === main.swift ===
#if BRAND_A
let brand = "Acme"
#elseif BRAND_B
let brand = "Globex"
#else
let brand = "Default"
#endif
print(brand)
  • Acme Globex Default
  • Default three times
  • Acme three times

Mental model. A franchise has one recipe book and one kitchen design; each shop differs in sign, menu extras and local suppliers. If a shop starts rewriting the recipe, you have stopped running a franchise.

The goal is many apps that differ only where they must, with the shared core staying one codebase. Separate what varies into layers. Identity: each brand that ships in its own App Store listing needs its own bundle identifier, signing identity and profile, push keys, associated domains, app icon and store metadata (Q478, Q479). Look: design tokens (colours, typography, spacing, icons) in a theme object injected through the environment, with brand assets in per-brand asset catalogs, so views never name a colour directly. Behaviour: a typed brand configuration (feature set, endpoints, legal text, default locale, flags) loaded at startup, so the core asks “is this feature enabled” instead of “which brand am I”. Code differences: prefer configuration and protocols; use compile-time flags only when a brand must not contain another brand’s code at all (a licensing or size reason). The check below builds the same source three ways with -D flags and gets three different brands, with the other brands’ code excluded from each binary. Structure the project as shared Swift packages for the core and features (Q362), one thin app target per brand (or one target with brand-specific schemes and .xcconfig files, Q369), and keep brand-specific code in its own module that the core never imports.

The costs are in the matrix and the process. A bug in the core ships to every brand, so tests run against a brand matrix (at least a representative subset in CI, all brands before release), snapshot and accessibility checks per brand (Q448, Q477), and the release train covers all apps with brand-level flags for staggered launches (Q487, Q486). Automate creating a brand (a script that generates the target, assets placeholder, bundle ID, CI job and store metadata skeleton) so that adding the fifth brand takes days, not weeks. Fight the fork: when a brand wants a one-off, ask whether it is configuration, a feature other brands might use, or a genuine extension point, and add that seam in the core rather than copying code; track brand-specific code as debt with owners. Choose a single binary with runtime theming only when all brands share one App Store listing; separate listings need separate binaries. The trade-off to state: compile-time separation gives smaller, cleaner binaries and harder isolation; runtime configuration gives faster builds and a smaller matrix but ships all brands’ code.

Each build includes only the code for its flag, which is how a brand can exclude another brand’s code.

Common traps

  • Scattering if brand == … through the code. Every new brand touches everything; inject configuration and theme instead.
  • Copying the project for each brand. Fixes must then be applied several times and the copies drift apart.

They might follow up with

  • When use compile-time flags? When a brand’s code or assets must not be in another brand’s binary, for licensing, size or confidentiality.
  • How do you test many brands? A brand matrix in CI, with the full set before release and shared tests parameterised by brand.

Related questions

  • 26.3.2 How do you design an SDK or shared module for other teams: surface area, configuration, errors, concurrency…
  • 26.4.4 How do you think about a platform organisation and multi-year technical bets: charter, paved road, build…
  • 15.6.2 How do you run a shared mobile platform for many apps and teams: layering, ownership, versioning and…
  • 16.4.1 What are build configurations and build settings, how do xcconfig files layer, and how does #if DEBUG work?
  • 20.1.3 How do you run one shared core with several UI leaves on a single release train?
  • 25.4.2 How do you run a release train for many apps: cadence, branch cuts, ownership, go/no-go criteria, hotfix…

How do you design an SDK or shared module for other teams: surface… →