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 byboot(), in the same round trip the session already cost. After that, the
client polls one public document:
- 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.
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 carriesstale-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, sorefreshFlags() is the
explicit read — for a server, for a test, or for the moment right after an act you
know flipped something.
@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, behindflags: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.