Documentation

Core Concepts

A short tour of the primitives you'll use every day: flags, rules, contexts, environments, segments, and evaluation reasons.

Flags

A flag is a named, typed value the SDK resolves at runtime. Every flag has:

  • A key: the string identifier you pass to evaluate().
  • A type: boolean, string, number, or json.
  • A default value: served when the flag is disabled, or when no rule matches.
  • An enabled switch, the kill switch. When off, every evaluation returns the default.
  • An ordered list of targeting rules.

Rules

All targeting lives in rules. A rule has an ordered list of conditions (all of which must match), a serve value, and an optional rollout percentage.

There is no flag-level rollout. A percentage rollout is a property of a rule. To ramp a flag to 10% of everyone, add a rule with no conditions and a rollout of 10.

How evaluation runs

The engine walks the rules in order and takes the first decisive one:

  1. If the flag is disabled, return the flag's default with reason flag_disabled. Done.
  2. For each rule in order:
    • If not every condition matches the context, skip to the next rule.
    • If the rule has no rollout, serve its value with reason rule_match. Done.
    • If the rule has a rollout, compute the user's bucket. Inside the percentage, serve its value with reason rollout. Done.
    • Outside the percentage, fall through to the next rule.
  3. If no rule was decisive, return the flag's default with reason default.

That fall-through is the subtle part, and it is useful: a user who misses a 10% rule is still eligible for every rule beneath it. Order your rules accordingly.

Evaluation context

The context is a flat bag of attributes you pass at evaluation time. Rules read from it. Values may be strings, numbers, or booleans, nested objects and arrays are not part of the attribute model.

  • userId: the bucketing key for percentage rollouts.
  • email, plan, country: common targeting dimensions.
  • Anything else you want, custom attributes are first-class.
Bucketing is sticky. The bucket is sha256("{userId}:{flagId}"), first 8 hex characters read as an integer, mod 100. Same user plus same flag always lands in the same bucket, on the server and in every SDK, so users never flip between variants on refresh. If userId is absent the engine falls back to email, and then to the empty string, which buckets every anonymous user identically.

Segments

A segment is a named, reusable audience you can reference from any rule with the segment_match operator, instead of copying the same conditions into a dozen flags. Membership comes from three places, and exclusion always wins.

  • Included keys: always in.
  • Excluded keys: always out, overriding both of the others.
  • Rules: a user matches if any rule matches (and a rule matches when all of its conditions do).

See the Segments guide for worked examples.

Environments

Each project has independent environments (typically development, staging, and production), each with their own API keys and flag configurations. An SDK key is bound to exactly one environment, which is how the SDK knows which flags to serve without you naming the environment anywhere in code.

Evaluation reasons

Every result includes a reason so you can explain why a flag returned what it did:

  • rule_match: a rule's conditions matched and it had no rollout.
  • rollout: a rule matched and the user fell inside its rollout percentage.
  • flag_disabled: the kill switch is off; the flag's default was returned.
  • default: no rule was decisive; the flag's default was returned.
  • flag_not_found: no flag exists with that key; your default was returned.
Where flag_not_found comes from. The REST API answers an unknown key with HTTP 404, not a result body. The SDKs turn that into a flag_not_found result carrying the default you passed, so a typo in a flag key degrades to your fallback instead of throwing.

Result shape

EvaluationResult
interface EvaluationResult {
  flagKey: string;
  value: boolean | string | number | Record<string, unknown>;
  reason: 'default' | 'rule_match' | 'rollout' | 'flag_disabled' | 'flag_not_found';
  /** UUID of the rule that decided this result — only on rule_match and rollout. */
  ruleId?: string;
}

Caching & freshness

The SDK keeps a local in-memory cache so synchronous get() calls return without a network round trip. The cache is populated by evaluate(), evaluateAll(), and the streaming transport. Flag changes propagate over SSE within seconds; when streaming is unavailable the client polls instead (30s by default).

That cache is also the SDK's failure story: it is served as last-known-good when a request fails, so an outage never silently reverts your users to compile-time defaults. See Resilience for the exact behavior per failure class.