Skip to main content
A flag answers “has this shipped to this person yet”. It is a release question about your product — not “does their plan allow it”, which is entitlements, and not “does their seat allow it”, which is roles and permissions. Keeping the three apart is why a flag check is free and why serving one is never billed: a kill switch that stopped working because an invoice was late would fail at the worst possible moment.

Reading one

isEnabled and useFlag are synchronous, and return a plain boolean. That is the contract rather than an implementation detail. A hook that started at undefined would render every gated feature’s absence for one frame and the feature on the next — and “we do not know yet” and “off” look identical to the person while only one of them is safe. A flag that flickers on is worse than one that is simply off. An unknown key is false. A flag nobody defined is a feature nobody shipped, and a page that never called boot() sees false for everything, which is the honest answer: nobody has decided this person’s audience.

What a flag is made of

null and 0 being different is the kind of distinction this API keeps rather than collapses: “not ramped” and “ramped, currently at zero” are different states of a release, and storing 100 for the first would lose the second. A flag targets a segment and never a filter of its own. That is the rule that keeps audiences from being rebuilt module by module — everything you can express about who sees a feature, you express once, in a segment, and every other module reads the same one.

The percentage, and which string it hashes

A rollout buckets on the contact id once somebody is identified, and on the device’s anonymous id while they are a visitor. Both are stable for as long as the thing they name is. Each flag carries its own salt, so being in the unlucky tenth of one rollout says nothing about the next — one shared salt would make every 10% rollout hit the same tenth of your users, who would then experience your product as permanently broken while nobody else could reproduce anything they reported. One flicker is real and is stated rather than hidden. An anonymous visitor buckets on their device; the moment they sign in, the same human buckets on a contact id — a different string, so possibly a different bucket, so a rollout they were inside may be outside one page load later. It is accepted, because the alternatives are worse. Bucketing everybody on the contact id would not avoid it (an anonymous visitor has a contact row, and signing in merges it away, so the id changes anyway). Carrying the anonymous id forward for life would make a rollout’s audience depend on a cookie the person can clear, which turns “10% of users” into “10% of browsers, resampled whenever somebody clears their history” — a percentage that drifts quietly is worse than one that changes once, at a moment the person is already watching the page change. What follows for you is one sentence rather than a mechanism: do not gate a checkout on a percentage flag for signed-out traffic.

The kill switch, and its honest delay

The flag’s value for this person — segment, percentage, everything — is decided by boot(), in the same round trip the session already cost. After that, the client polls one public document:
It carries what is true for everybody — the switch — and nothing about who. Applied over the boot snapshot, it can only ever take a flag away:
  • turning a flag off reaches an open page in about fifteen seconds, and no session refresh is involved;
  • widening a rollout, or re-aiming a segment, takes effect on the next boot.
That asymmetry is the design. The urgent direction is off — “the checkout is broken, turn it off now” is a sentence with a clock attached, and “everybody in the new segment should get this” is not. It is also why a flag is not a JWT claim. A five-minute token makes the answer eventual by construction, and a kill switch that cannot take effect for five minutes is not a kill switch. A push — a socket, a stream — would be faster and is the wrong trade at this size: a connection per open page, held through every deploy, whose failure mode is your product silently not hearing about flips and whose recovery is a poll anyway. The document is cacheable, addressed by the publishable key your page already holds, and safe behind a CDN. version is opaque — compare it for equality to learn nothing moved, never for order.

What happens when the network is not there

A failed refresh keeps the last document. It never empties the flags. A kill switch that opens whenever the network drops is the opposite of what it exists for, and degrading to “no flags” during a bad minute would roll your product back to its pre-flag state for everybody — our outage becoming yours, over a feature you turned on last week and have not thought about since. The document carries stale-if-error beside its max-age for the same reason. The poll runs only while the page is visible; a backgrounded tab is not a page somebody is about to be surprised by. An archived flag disappears from the document, and a key that is not in it is false. Archiving is how you finish a rollout you decided against — for readers it is indistinguishable from a delete, which is what you want.

On a server

There is no interval and no visibility to key one off, so refreshFlags() is the explicit read — for a server, for a test, or for the moment right after an act you know flipped something.
With @userkit/nextjs the same document is fetched through your own origin, so the publishable key stays out of the bundle.

In the panel

Engagement → Flags, behind flags:manage — owner and admin by default. One permission covers the whole surface here, unlike segments, where reading is customers:read because a segment’s read is a filtered list of contacts. A flag’s read is not a view of anything another permission already shows: it is the release state of your product, so it has no other home. It is a separate permission from segments:manage precisely because the two are adjacent. Defining an audience is describing people; flipping a flag is shipping or unshipping a feature in the minute it is flipped, for whoever is holding the page. An organization that wants its growth analyst building audiences without the ability to turn the checkout off says so by granting one and not the other. Flipping a flag publishes flag.updated on the internal bus, and that fact deliberately does not leave the building as a webhook: it names an environment’s release state, and the document is already the supported way to learn about a flip. The bucketing salt is never returned by the API. It is the one value that would let somebody compute which bucket they are in and then pick an identifier that lands inside a ramp.