tier·dev

conditional compilation alternatives for modern web apps

comparison8 june 2026· 6 min read

for web products that ship multiple editions, the practical alternative to c-style conditional compilation is per-tier flag config plus your bundler's dead-code elimination: gate features with flag checks, resolve flag values per tier at build time, and let minification strip the disabled branches. you get edition-specific artifacts without preprocessor directives or parallel build systems.

why ifdef culture doesn't translate

conditional compilation earned its keep in c and c++: platforms genuinely differed, binaries had to be lean, and the preprocessor was the only tool. porting that culture to web apps brings the costs — code that doesn't parse as written, combinatorial build matrices, editor tooling that can't follow the branches — without the payoff, because javascript engines and bundlers already optimize at a different layer.

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, let the minifier drop dead branches. pure artifacts, but values live in build scripts — opaque to product review.
  • runtime flag services: maximum flexibility, but they add a network dependency and an sdk — wrong shape for an open-source distribution that must work standalone.
  • per-tier flag config in git: a committed tiers.json resolved at build (or boot). reviewable like code, 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.

see this on your own repo

tier·dev turns the open-source / saas difference into per-tier feature flags — one repo, generated config, no sdk.

faq

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.

keep reading

← all guidestry tier·dev