> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Feature flags

> A flag decided once at boot, kept current by a document that can only ever turn one off — and why the urgent direction is off.

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](/en/guides/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

```ts theme={null}
import { createClient } from "@userkit/js";

const userkit = createClient({ publishableKey: "uk_pk_live_…" });
await userkit.boot();

if (userkit.isEnabled("new-checkout")) {
  // …
}
```

```tsx theme={null}
import { useFlag, Flag } from "@userkit/react";

function Checkout() {
  return (
    <Flag name="new-checkout" fallback={<LegacyCheckout />}>
      <NewCheckout />
    </Flag>
  );
}
```

`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

| Field                | Meaning                                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------------------- |
| `key`                | What your code reads. Unique per environment                                                            |
| `enabled`            | The outer gate. `false` is off for everybody — no segment consulted, no bucket computed                 |
| `segment_id`         | The audience, or `null` for everybody                                                                   |
| `rollout_percentage` | A percentage **of that audience**, evaluated after it. `null` is no ramp; `0` is a ramp reaching nobody |

`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](/en/guides/segments), 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:

```http theme={null}
GET /v1/flags/{publishable_key}
```

```json theme={null}
{
  "flags": [{ "key": "new-checkout", "enabled": false }],
  "version": "…",
  "max_age_seconds": 15,
  "generated_at": "2026-08-02T12:00:00Z"
}
```

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.

```ts theme={null}
await userkit.refreshFlags();
```

With [`@userkit/nextjs`](/en/customer-auth/session-tokens) 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.
