The pattern
A piece of code wrapped in if (flag('new-checkout')) { ... } else { ... }. The flag value comes from a config store that can be updated without a deploy — environment variable, database row, or a managed service like LaunchDarkly or Vercel Edge Config.
What flags enable
- Gradual rollout. Ship a new feature to 1% of users, watch for errors, ramp to 10%, then 100%. Standard practice for anything risky.
- Kill switches. If something breaks in production, flip the flag off without a hot deploy.
- A/B testing. Show variant A to half of users, variant B to the other half. Measure outcomes.
- Customer-specific configuration. Customer X gets the new dashboard; customer Y doesn't, because their CSM hasn't onboarded them yet.
- Long-running branches deployed-but-hidden. Build a half-finished feature, ship it gated, finish later.
When to add them
When you have real users and real risk of breaking them with a deploy. Pre-launch, flags are usually overhead — just deploy.
A common mistake: setting up a flag system at v1 because "we might need it later." We probably won't need it for the validation tier of an MVP. Add the system when the first real rollout question shows up.
Defaults we use
- Tiny flag count: Vercel Edge Config or a hardcoded TypeScript config map
- Per-user / per-workspace flags: a flags table in Postgres with simple RLS
- A/B testing: PostHog feature flags (free tier good)
- Enterprise: LaunchDarkly (overkill until you have enterprise customers)
Cost of bad flag hygiene
Old flags that nobody removes accumulate into "flag debt." Every conditional branch is a code path. Conditionals nested in conditionals create exponentially many code paths. A two-year-old codebase with 80 active flags has more states than anyone can test. We delete flags after rollout is 100% — no exceptions.


