Skip to content
cloudhostDocs
Products

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 list

In 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).

A change reaches every server within seconds and does not restart functions. A change to the development environment is not sent to servers, since nothing deployed reads it; it is read with that environment's read-only token.

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.

Code
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);
  },
};
MethodWhat 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
Reading a flag never throws: if the flags are missing or the flag does not exist, your default comes back.

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).

CLI
cloudhost flags set new-checkout true --environment preview --branch feature/checkout
cloudhost flags eval new-checkout --environment preview --branch feature/checkout

Rules and rollouts#

A rule has conditions that must all hold and the value served when they do. The conditions:

ConditionMeaning
equals, not_equalsthe attribute equals one value, or does not
in, not_inthe attribute is in a list, or is not
contains, not_containstext contains the substring; for a list attribute, it holds the element
starts_with, ends_withtext 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 key by default; use --by or bucket_by for another (say orgId) so a whole organisation moves together. A context without that attribute skips the rollout.
  • Change the salt to 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 true
A segment a flag rule uses cannot be deleted; remove it from the rule first.

The 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.

CLI
cloudhost flags kill new-checkout                         # every environment
cloudhost flags kill new-checkout --environment production
cloudhost flags revive new-checkout

History 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.

CLI
cloudhost flags history new-checkout
cloudhost flags rollback new-checkout 3

Read 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 })) { … }
Each token has a limited number of reads per second (by plan); keep answers in your app for a few seconds. A revoked token stops working within five seconds.

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)HobbyPro
Flags201,000
Rules per flag and environment550
Segments2100
Changes a month500Unlimited
History kept30 days1 year
Branch settings per flag5100
Read-only tokens220
Reads per second per token10100
Flag requests a month (OpenFeature)100,0001,000,000

A value is at most 2 KiB and one config at most 16 KiB. GET /v1/pricing lists these limits.