The short version
- Shipping a requested feature fast to everyone is a gamble: users may not like it, the flow may be wrong, people may not understand it — and walking back a public feature costs more than building it did.
- Our pattern: build the host-side functionality backend-complete, add the minimal guest-side UI that makes the loop work end to end, and put the whole thing live in production behind flags. The requester gets their feature in days; everyone else gets nothing until we've watched it work.
- The flags aren't one switch. They're a ladder of scopes — dev environment, local env, remote config, a specific user, a specific event, a specific profile — and a feature walks up the ladder as feedback comes in, until the last rung is “everyone.”
The daily problem: requests arrive faster than launches should
Every product team knows this inbox. A host writes in: "Can I price my tickets in tiers — early bird, regular, late?" It's a good request. It's also a feature that touches pricing, checkout, and what every guest sees at purchase time.
The fast answer — build it, launch it, announce it — carries three failure modes we've been burned by:
- People may not like it. The feature you imagined from the request isn't quite the feature they wanted.
- The flow may be wrong. Your first UI guess is a guess. At scale, a wrong guess is thousands of confused sessions.
- Users may not understand it. A feature that needs explaining to strangers needed another iteration before strangers saw it.
The slow answer — roadmap it, design it, test it for a quarter — loses the thing that made the request valuable: a real user, asking right now, willing to use it and tell you what's wrong.
Our answer sits in between: ship it completely, launch it to almost nobody.
Backend-complete, guest-side minimal
The build itself follows one split. The host side — the person who asked — gets full backend capability: creation, validation, rules, permissions, edge cases. The guest side — the people who encounter the feature — gets the minimal UI that makes the loop real: enough for a guest to see and use what the host configured, nothing more.
That combination is what makes the feature complete rather than half-built. The requesting host can actually run it, their guests actually experience it, and the whole loop produces real feedback — while the surface area exposed to the world stays tiny. We build faster because we're not designing speculative dashboards, the host's request gets fulfilled in days, and the feedback we collect comes from real usage instead of a prototype session.
Backend-complete, guest-side minimal
host side — complete
The person who asked gets full backend capability:
- creation
- validation
- rules
- permissions
- edge cases
guest side — minimal
Enough for a guest to see and use what the host configured — nothing more. Few screens, not rough ones.
No speculative dashboards, no imagined settings pages.
But none of that works unless the feature can be live in production without being live for everyone. That's the flag system, and the interesting part is that "a feature flag" turned out to be six different things.
The flag ladder: six kinds of "on"
| Level | Mechanism | Who sees the feature |
|---|---|---|
| 1. Dev / test | Env-backed flag, defaults on in local and emulator | Developers, automatically |
| 2. Production kill switch | Same flag, defaults off in prod until explicitly "true" | Nobody, until ops says so |
| 3. Remote config | Config service the client fetches at startup | Whoever the config targets — no deploy needed |
| 4. Specific user | Allowlist of identity ids checked at request time | A handful of named design partners |
| 5. Specific profile | feature_flags map stored on the profile document | One host profile or org, everywhere they act |
| 6. Specific event | Flag stored on the event document itself | Guests of exactly one event |
Six kinds of “on”
- 1dev / testdevelopers, automaticallyenv flag, defaults on in local and emulator
- 2production kill switchnobody, until ops says sosame flag, off in prod until explicitly "true"
- 3remote configwhoever the config targetsfetched at startup — no deploy needed
- 4specific usera handful of named design partnersallowlist of identity ids, checked per request
- 5specific profileone host or org, everywhere they actfeature_flags map on the profile document
- 6specific eventguests of exactly one eventflag stored on the event document itself
Each level exists because a question at a different scope kept coming up.
Levels 1–2 are the same flag with environment-aware defaults. Our server-side flags read an env var: explicitly "true" is on, explicitly "false" is off — and unset defaults to on in dev and test, off in production. That one rule means developers never configure anything locally, while production never turns something on by accident. (Risky subsystems get the stricter variant: off everywhere unless explicitly enabled, because "convenient in dev" is wrong when the dev default would call real infrastructure.)
Level 3 is the no-deploy switch. The client fetches remote config at startup and feeds it into app state, so a flag flip reaches users at next load without a release. This is the rung for coordinated reveals and fast retreats — when the thing you're adjusting is exposure, not code.
Level 4 is the design-partner rung. A comma-separated allowlist of identity ids, parsed once at boot into a set, checked in O(1) per request. When we rolled out write access on our public API, reads went to everyone while writes went to two named identities — the integrators who'd asked — before anyone else. Empty allowlist means "fall back to the global flag," so the same code path serves both the pilot and the eventual full launch.
Level 5 keys the feature to a profile. A feature_flags map on the identity document — enable tiered pricing for this host, and it's available on everything they run, wherever they act. This is the rung for "the requester gets the feature," and because flags attach to identities rather than auth accounts, enabling an org enables exactly the org.
Level 6 is the narrowest and the one people don't build: per-event flags. A flag on the event document itself. The feature exists for this one event's guests and no one else's. It's the perfect first-exposure rung — the requesting host tries the feature where real guests will hit it, and the blast radius is one event.
Precedence: the rungs have to compose
With flags at multiple scopes, the same feature can be "on" at one level and "off" at another, so the order of evaluation is part of the design. Ours, for the gated feature:
// 1. Event says yes → enabled for this event.
if (event.featureFlags?.tieredPricingEnabled) return allowed
// 2. Event says NO, explicitly → disabled. Full stop.
// An allowlisted host can still opt one event out.
if (event.featureFlags?.tieredPricingEnabled === false) return forbidden
// 3. Event is silent → fall back to the host profile's flags.
if (identityFlagEnabled(hostProfile.feature_flags, TIERED_PRICING)) return allowed
return forbiddenThe subtle rule is step 2: an explicit false at a narrow scope beats an allow at a wider scope. A host who's been granted the feature globally can still turn it off for one event — without that, "enable it for this profile" would mean "force it on everything they run," and the first host to want an exception would be stuck.
Precedence — try it
The event's flag
The host profile's flag
- 1Event says yes → enabled for this event
- 2Event says no, explicitly → disabled. Full stop.decides here
- 3Event is silent → fall back to the host profile's flags
forbidden — the narrow false beat the wide allow: one event opted out
How a feature actually walks the ladder
The tiered-pricing request, end to end:
- Backend ships complete — families, variants, validation, the guest-side purchase path — flagged off in production, on in dev. Nobody outside the team knows it exists.
- The requesting host's profile gets the flag. They configure tiers on their next event; their guests buy through the minimal guest UI. First real feedback arrives within a week — and some of it stings: a label that confused people, a flow step hosts did in the wrong order.
- We fix what the feedback surfaced. The fix ships to the same one audience. Iterate.
- More hosts ask (requests are also votes). Each gets the profile flag; now the feedback has variety.
- When the confusion reports stop, the flag widens — remote config reveals the UI, the global switch flips, and the "launch" is an announcement of something already proven in production.
How a feature walks the ladder
- 1Ship it completethe team onlyFlagged off in production, on in dev. Nobody outside knows it exists.
- 2The requester's profile gets the flagone host + their guestsThey configure it on their next event; first real feedback arrives within a week — and some of it stings.
- 3Fix what the feedback surfacedthe same one audienceThe confusing label, the step hosts did in the wrong order. Iterate.
- 4Requests are votesa few named hostsEach new asker gets the profile flag; now the feedback has variety.
- 5Widen, rung by rungeveryoneRemote config reveals the UI, the global switch flips — the "launch" announces something already proven in production.
The launch-day risk at step 5 is close to zero, because every failure mode the big-bang launch would have discovered at scale was discovered at a scale of one.
What the flags taught us the hard way
- A flag's default is part of its design. Our worst flag incident wasn't a flag that was on — it was one that was unset in a pre-prod environment, silently disabling a whole subsystem that returned generic 404s with no log line saying why. Rule since: every flag rejection logs itself, and every flag has an explicit documented default per environment.
- Flags rot into archaeology. A flag that's been 100%-on for six months is dead code wearing a switch. Widening to "everyone" now comes with a removal task — the flag's last state transition is deletion.
- Never expose the flag snapshot publicly. A map of which features are off is a map of what's coming and what's broken. Ours is readable by admin surfaces only.
- The minimal guest UI still has to be good. Guests at the pilot event are real people, not beta testers who opted in. Minimal means few screens, not rough ones.
Steal my workflow
- Split every requested feature into host-side capability (build it complete) and guest-side UI (build the minimum that closes the loop).
- Build the flag ladder before you need it: env flags with environment-aware defaults, remote config, an identity allowlist, a
feature_flagsmap on profiles, and a flag field on the content object itself. - Make narrow-scope explicit
falsebeat wide-scope allow, so grants never become mandates. - Deliver the feature to the requester at the narrowest rung that works — their profile, or their single event.
- Treat requests as votes and feedback as the spec for the next iteration; widen one rung at a time.
- Log every flag rejection with the flag's name. Silent "off" is indistinguishable from broken.
- When a flag reaches "everyone," schedule its deletion.
FAQ
Because the levels answer different questions, and most flag services only answer two of them. "Is this subsystem safe to run here" is an env concern; "should this guest see this UI" is a data concern that lives naturally on the profile and event documents we're already reading. The allowlist and document flags cost us a few dozen lines each and zero extra reads.

Written by
Rushit Jivani
Senior Software Architect & AI Application Engineer. Architecture that shows up in crash rate, performance and how fast the team ships.