All docs
Feature flagsIntroduction to feature flagsdashboard

Introduction to feature flags

What a gate is, the two credentials, and what a flag actually evaluates to.

What you'll have

A clear picture of how a flag resolves, and which credential belongs where.

Turn a feature on for everyone, for one user, or for nobody, and have the change take effect in about a second without a deploy.

A feature flag in variA/Bly is a gate you create under a project. Your application asks for it by key and gets a value back. Nothing about the flag is compiled into your build, so changing it is a change to the dashboard, not to your code.

Three things are called "key"

Worth separating before anything else, because they turn up together and are not interchangeable.

TermWhat it isLooks like
Gate keyThe flag's identifier, and what your code asks forcheckoutRedesign
API keyYour secret server credentialvb_live_…
Context keyThe user identifier you pass when evaluating{ key: user.id }

The rest of this page says "gate key" and "API key" in full. The context key is the one that catches people: allFlags({ key: user.id }) is asking "what does this user get", and has nothing to do with either credential.

The gate key is the contract

A gate's gate key is the exact string your code reads. It is compared byte for byte, so checkoutRedesign and checkoutredesign are two different flags.

Set the gate key deliberately when you create the gate. The Create Gate form derives a suggestion from the display name and lowercases it, so typing "checkoutRedesign" as the name gives you the gate key checkoutredesign unless you say otherwise. Type the gate key you want your code to use, and the whole question disappears.

The reason to care: a gate key that does not match reads as a flag that does not exist. If your application has a fallback for that case — an old flag system, a hardcoded default — the page renders plausibly and nothing reports a problem. Name it right at creation and this never comes up.

Two credentials, and they are not interchangeable

API keyClient-side ID
WhereAPI KeysSettings → Project → Environments
Secret?Yes — server onlyNo — publishable
ReachesEvery variA/Bly featureFlag evaluation only
SeesEvery flag in the projectFlags not hidden from client-side SDKs

Your server uses the API key. It reaches prompts, dynamic configs and LLM execution as well as flags, which is exactly why it must not travel to a browser.

Your browser uses the client-side ID. It ships in your JavaScript bundle, and that is fine: it evaluates flags and nothing else. Give it to @varia-bly/react and a flag toggled in the dashboard reaches a page that is already open.

Both carry an environment, and that is how a flag gets a different answer in each one. Each environment has its own client-side ID; each API key is created against one environment. Nothing in your code names an environment — the credential does, so promoting a build from staging to production is a change of key, not a change of code.

Worth being exact about what that scoping is, because the word suggests more than it does. An API key's environment decides which environment's flag values it is answered with. It is not a permission boundary: every key in a project reaches the same resources, and prompts, LLM evaluation and experiments behave identically whichever environment a key names. Only flag evaluation reads it.

See Environments for how to create them and what each one owns.

What a flag evaluates to

Three things decide the value, in this order.

  1. A user override for the context you passed. Matched on user id.
  2. The first rule that matches the context.
  3. The flag itself. Enabled serves true; disabled serves the Default value you set on the gate.

That third line is worth reading twice. Turning a flag off does not automatically serve false — it serves whatever you put in Default value. For a boolean gate you almost always want that to be false, which is what a new gate starts with.

A missing flag is an error, not a default

Ask for a gate key the project does not define and the SDKs raise FlagNotFoundError rather than handing back the default you passed.

This is deliberate. A typo, an unseeded environment or a half-finished migration would otherwise look identical to a flag that is deliberately off: the application runs on defaults and appears healthy. Call validateFlags() at start-up with the gate keys you expect and every missing one is reported at once, before a request path reaches whichever is read first.

Where to go next

Wire it into an application in Add flags to your app, or read what every field on a gate does in Flag settings.