The three modern alternatives
Each alternative trades differently between artifact purity and simplicity.
Build-time defines (webpack DefinePlugin, esbuild define, Vite define) inline a constant and let the minifier drop dead branches — pure artifacts, but values live in build scripts, opaque to product review. Runtime flag services give maximum flexibility, but they add a network dependency and an SDK — the wrong shape for an open-source distribution that must work standalone.
Per-tier flag config in git is a committed tiers.json resolved at build (or boot). It's reviewable like code, carries no runtime vendor, and it feeds the same define mechanism when you want true stripping.
When you genuinely need code absent
Sometimes 'disabled' isn't enough — the open-source artifact must not contain the paid code at all. The flag-config approach handles this too: because flag values are known at build time, you can point your bundler's define at the resolved tier config, and dead-code elimination removes the gated modules from the open-source bundle entirely. One mechanism covers both shipping dark and shipping absent.
The Knight Capital incident is the cautionary tale for doing any of this implicitly. Flag state that lives in deployment scripts, environment lore, or a dusty server is invisible at review time. Flag state that lives in a committed, generated config file is a diff someone approves.
Choosing for a tiered product
If your editions are pricing tiers of one web product, per-tier flag config is the default choice: it's the only option where the packaging decision — who gets what — is first-class and versioned. tier·dev generates that config (JSON, YAML, or a TypeScript module) from a tier/flag matrix, so the build-time machinery stays three lines of bundler config.
Three conditional-compilation alternatives for web apps
| approach | artifact purity | reviewable in product terms? | runtime dependency |
|---|---|---|---|
| build-time defines | high — dead branches stripped | no — values hidden in build scripts | none |
| runtime flag service | low — code ships, gated live | yes — vendor dashboard | SDK + network |
| per-tier flag config in git | high when fed to defines | yes — committed, diffable config | none |
frequently asked
- Does dead-code elimination really strip whole features?
- Yes, when the gate is a build-time constant and the feature is import-reachable only through the gated branch. Verify with a bundle analyzer once, then trust the pipeline.
- What about server-side Node code?
- The same config works without bundling: read tiers.json at boot and branch on it. If absence matters server-side, exclude the module at packaging time using the same resolved values.
- Is this just '#ifdef with extra steps'?
- It's #ifdef with the steps that were always missing: a reviewable record of every edition's values, names with descriptions, and no preprocessor dialect inside your source files.
Published June 8, 2026 · Last updated June 16, 2026