Introduction to feature flags
What a gate is, the two credentials, and what a flag actually evaluates to.
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.
| Term | What it is | Looks like |
|---|---|---|
| Gate key | The flag's identifier, and what your code asks for | checkoutRedesign |
| API key | Your secret server credential | vb_live_… |
| Context key | The 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 key | Client-side ID | |
|---|---|---|
| Where | API Keys | Settings → Project → Environments |
| Secret? | Yes — server only | No — publishable |
| Reaches | Every variA/Bly feature | Flag evaluation only |
| Sees | Every flag in the project | Flags 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.
- A user override for the context you passed. Matched on user id.
- The first rule that matches the context.
- 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.