Feature flags in a single-tenant app are mostly about deployment safety: ship dark, roll out gradually, kill the switch if something breaks. In multi-tenant vertical SaaS, flags do that job plus a second one that's easy to underestimate at the start — they're also the mechanism for plan-gating and per-tenant customization, which means the flag system ends up load-bearing for both engineering safety and the billing model.
Two different kinds of flag, and why conflating them hurts
A release flag is temporary: it exists to de-risk a deploy and gets deleted once the feature is fully rolled out and stable. A plan flag is permanent: it exists as long as the pricing tier does, gating a real feature behind a paid plan. The mistake I've seen teams make is using the same flag for both — shipping a feature behind a "dark launch" flag, then repurposing that same flag as the permanent plan gate once the feature ships. Six months later nobody remembers which flags are safe to delete and which ones are load-bearing for billing, and cleanup becomes too risky to attempt.
If a team is scared to delete an old release flag because "something might depend on it," that's a sign the flag was never cleanly separated from permanent plan-gating logic. Keep the two flag types in visibly different namespaces (release. vs plan. prefixes) so a stale flag audit is a five-minute grep, not an investigation.
The evaluation key should usually be the tenant, not the user
Most flag SDKs default to evaluating flags per user, which makes sense for consumer products where each user has an independent experience. In B2B vertical SaaS, the buying unit is the tenant (the company), and inconsistent behavior between two users at the same customer — one sees a new UI, their coworker doesn't — generates support tickets that have nothing to do with the actual feature. Evaluate plan-gating flags on tenant ID as the primary key, and only fall back to user-level targeting for genuinely user-scoped experiments like a UI redesign A/B test.
function isFeatureEnabled(flagKey: string, ctx: { tenantId: string; planTier: string }) {
const flag = flagRegistry.get(flagKey);
if (!flag) return false;
if (flag.type === 'plan') {
return flag.enabledForPlans.includes(ctx.planTier);
}
// release flags: percentage rollout keyed on tenantId so every
// user at a tenant gets the same answer
const bucket = hashToBucket(ctx.tenantId, flag.salt);
return bucket < flag.rolloutPercent;
}
Rollback has to work while the tenant is actively using the feature
The point of a release flag is that flipping it off mid-incident doesn't require a deploy. That only holds if the flag is checked on every relevant code path with no caching that outlives a reasonable TTL, and if turning a flag off degrades gracefully rather than throwing — a tenant who had a feature enabled, then disabled mid-session, should fall back to the old behavior cleanly, not hit a broken UI referencing state that no longer exists.
Flag debt accumulates faster in multi-tenant systems
Because plan flags are permanent by design, the flag count in a mature vertical SaaS product only grows — every new plan tier or add-on module is another flag that never gets deleted. The practical fix is a quarterly audit: any release flag rolled out to 100% for more than one quarter gets deleted and its condition inlined, and plan flags get consolidated into a single tenant-entitlements table rather than living as scattered boolean checks through the codebase.
Wrapping up
Feature flags in multi-tenant SaaS are doing two jobs — safe rollout and plan enforcement — and treating them as one undifferentiated pile of booleans is how flag debt becomes unmanageable. Separate the namespaces, evaluate plan-gating on tenant ID, and delete release flags on a schedule instead of never.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.