Feature flags
Turn features on and off without deploying, open them to a share of users or target a group. Flag values travel to the servers with the configuration, so reading one makes no network call.
What a flag is#
A flag belongs to an app or function and has a key and a type: boolean, string, number or json. It has separate settings for each environment (production, preview, development), and any preview branch can have its own.
An environment's settings are checked in order: a killed flag serves its off value (off_value); otherwise the first matching targeting rule; then the percentage rollout; finally the default value.
Create and change#
cloudhost flags add new-checkout --type boolean --description "New checkout flow"
cloudhost flags set new-checkout true --environment preview
cloudhost flags rule add new-checkout email ends_with @acme.ir --value true --environment production
cloudhost flags rollout new-checkout 10 --value true --environment production
cloudhost flags listIn the dashboard, flags are under “Flags” in the project's sidebar: a list, and for each flag a page with its environments, rules, rollout slider, a test and its history. The MCP server has the same operations (list_flags, set_flag, kill_flag and the rest).
Read in code#
Every function and app has env.FLAGS. It needs no token and makes no network call: it reads from the server itself. context holds the attributes rules check; its key (or the attribute you chose for the rollout) decides which group each user is in.
export default {
async fetch(request, env) {
const user = { key: "user-42", email: "a@acme.ir", plan: "pro" };
// the value; false if there is no such flag
if (await env.FLAGS.get("new-checkout", false, user)) return newCheckout(request);
// every flag at once, or why a flag gave its answer
const all = await env.FLAGS.getAll(user);
const why = await env.FLAGS.evaluate("new-checkout", user); // { value, reason, rule? }
return oldCheckout(request);
},
};| Method | What it does |
|---|---|
await env.FLAGS.get(key, default?, context?) | The value; your default if the flag does not exist |
await env.FLAGS.getAll(context?) | Every flag: {key: value} |
await env.FLAGS.evaluate(key, context?, default?) | {value, reason, rule?, variant?, bucket?}; reason is off, rule, rollout, default or missing |
Environments and branches#
Production follows the production settings and previews follow preview. A branch can have settings of its own: that branch's previews follow them instead of preview (the branch is named as in git push).
cloudhost flags set new-checkout true --environment preview --branch feature/checkout
cloudhost flags eval new-checkout --environment preview --branch feature/checkoutRules and rollouts#
A rule has conditions that must all hold and the value served when they do. The conditions:
| Condition | Meaning |
|---|---|
equals, not_equals | the attribute equals one value, or does not |
in, not_in | the attribute is in a list, or is not |
contains, not_contains | text contains the substring; for a list attribute, it holds the element |
starts_with, ends_with | text starts or ends with the substring |
- Rules are checked in order and the first match wins; with a
percentage, only that share of the matches get it. - Rollout: each key is hashed to a fixed number from 0 to 99.99 percent, so a user always stays in the same group, on every server and over REST. Raising the percentage only adds users; nobody who had the flag loses it.
- The grouping attribute is
keyby default; use--byorbucket_byfor another (sayorgId) so a whole organisation moves together. A context without that attribute skips the rollout. - Change the
saltto reshuffle who is picked.
Segments#
A segment is a list of users you reuse across flags: users included, users excluded (excluded wins), and rules for everyone else. In a flag rule, use the in_segment or not_in_segment condition with the segment's key.
cloudhost flags segments add staff --include user-1,user-2
cloudhost flags rule add new-checkout key in_segment staff --environment production --value trueThe kill switch#
If a feature breaks, kill its flag: the off value is served at once and rules and rollouts are ignored. No redeploy and no editing of rules; revive it when it is fixed.
cloudhost flags kill new-checkout # every environment
cloudhost flags kill new-checkout --environment production
cloudhost flags revive new-checkoutHistory and rollback#
Every change makes a version in the flag's history: who, when, what, and which environments changed. Rolling back to a version puts the whole flag (every environment and branch) back as it was, and is recorded too. The history stays after a flag is deleted, so a deleted flag can be brought back.
cloudhost flags history new-checkout
cloudhost flags rollback new-checkout 3Read from outside the platform#
For your own server or tool, create a read-only token; each token sees one environment and can change nothing. Targeting rules stay with us and only the answers come back.
// flags.js: a small client, no dependencies
const API = "https://api.ibel.ai";
const cache = new Map();
export function flags(token, { ttl = 5000 } = {}) {
const ask = async (key, context) => {
const id = key + JSON.stringify(context ?? {});
const hit = cache.get(id);
if (hit && Date.now() - hit.at < ttl) return hit.value;
const r = await fetch(`${API}/v1/flags/read`, {
method: "POST",
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
body: JSON.stringify({ keys: [key], context }),
});
if (!r.ok) throw new Error(`flags: HTTP ${r.status}`);
const value = (await r.json()).flags[key];
cache.set(id, { at: Date.now(), value });
return value;
};
return {
get: async (key, fallback, context) => (await ask(key, context))?.value ?? fallback,
evaluate: (key, context) => ask(key, context), // { value, reason, rule? }
};
}
// const f = flags(process.env.FLAGS_TOKEN);
// if (await f.get("new-checkout", false, { key: user.id })) { … }OpenFeature#
Apps running outside the platform read flags with the standard OpenFeature SDKs and their OFREP provider. Create a flag key: a server key sees every flag of one environment; a browser key sees only the flags you made visible to browsers, so it is safe to put in a web page.
import { OpenFeature } from "@openfeature/server-sdk";
import { OFREPProvider } from "@openfeature/ofrep-provider";
await OpenFeature.setProviderAndWait(new OFREPProvider({
baseUrl: "https://api.ibel.ai",
headers: [["Authorization", `Bearer ${process.env.FLAG_KEY}`]],
}));
const client = OpenFeature.getClient();
const on = await client.getBooleanValue("new-checkout", false, { targetingKey: user.id, plan: user.plan });targetingKey is the rollout key, so each user always gets the same answer. Each request counts as one flag request, however many flags it reads; usage and price are on the pricing page.Limits#
| Limit (per app or function) | Hobby | Pro |
|---|---|---|
| Flags | 20 | 1,000 |
| Rules per flag and environment | 5 | 50 |
| Segments | 2 | 100 |
| Changes a month | 500 | Unlimited |
| History kept | 30 days | 1 year |
| Branch settings per flag | 5 | 100 |
| Read-only tokens | 2 | 20 |
| Reads per second per token | 10 | 100 |
| Flag requests a month (OpenFeature) | 100,000 | 1,000,000 |
A value is at most 2 KiB and one config at most 16 KiB. GET /v1/pricing lists these limits.