Skip to content

Contexts

A context is the thing you pass to the SDK on every flag evaluation. It says “here is who, or what, is asking.” Targeting rules match against fields on the context.

client.evaluate("checkout-v2", false, {
targetingKey: "user-123",
user: { plan: "pro", email: "alice@example.com" },
});

A context always has a targeting key, a string that uniquely identifies the thing being evaluated. Everything else is structured under one or more context kinds:

{
targetingKey: "user-123",
user: { plan: "pro", email: "alice@example.com" },
organization: { id: "acme", region: "eu" },
device: { os: "ios", appVersion: "4.2.1" }
}

You can pass any combination of kinds. A flag rule can reference any kind that exists in the project’s context kinds registry.

The targeting key is special. It is what feat hashes for percentage rollouts: the same targeting key always falls into the same bucket while the rollout weights are unchanged. So:

  • Use a stable per-user identifier (your internal user ID, an email if it never changes).
  • Do not use a value that changes per request (a session ID, a request ID): users will hop between variations on every page load.

For anonymous traffic, mint a stable random ID and persist it (cookie, localStorage, mobile keychain) so a user keeps the same bucket across sessions. The web SDK does this for you when configured with anonymous: { storage: "localStorage" }.

A single context can carry several kinds. This is how you express “the user is X, and their organization is Y, and their device is Z” in one evaluation. A rule can reach into any of them:

user.plan is one of ["pro", "enterprise"]
organization.id is one of ["acme", "beta-org"]
device.os is "ios"

All three are AND’d within the rule. See Targeting rules for the rule structure.

An anonymous context represents a visitor you have not identified yet. The shape carries a boolean on the user kind:

{
targetingKey: "8f3c9c2c-...",
user: { key: "8f3c9c2c-...", anonymous: true },
}

You still need a targeting key, but it is a random ID rather than your internal user ID. Persist it client-side (a cookie, localStorage, a mobile keychain) so the same visitor lands in the same percentage-rollout bucket on every page load. The web SDK does this for you when configured with anonymous: { storage: "localStorage" }:

const client = new FeatWebClient({
envKey: "FEAT-CLIENT-...",
anonymous: { storage: "localStorage" },
});

It mints a UUID on first run, writes it to feat:anonymousKey, and re-reads it on subsequent loads.

  • Marketing-site or landing-page traffic, before signup.
  • Mobile app first launch, before the user creates an account.
  • Edge or middleware code that fires before the auth layer has resolved a user.

anonymous is a regular boolean attribute under the user kind, so rules match it the same way as any other field:

user.anonymous is one of [true] → only anonymous visitors
user.anonymous is_empty → only identified users

A common pattern is to bucket anonymous traffic separately from logged-in users, so an experiment on the marketing site does not contaminate one running inside the product.

When a visitor signs in, swap the SDK context for the identified shape:

await client.setContext({
targetingKey: "user-123",
user: { plan: "pro", email: "alice@example.com" },
});

The targeting key changes, so the visitor enters a different percentage-rollout bucket on the next evaluation. If you want continuity across the login boundary (same bucket before and after), keep the previously-minted anonymous key as the targeting key and just attach the identified attributes alongside.

Anonymous-context evaluation works on every SDK because anonymous is a regular attribute. Auto-mint and persistence are only provided where they belong, on the client side:

SDKanonymous: true support
Web SDKAuto-mint and persist via anonymous: { storage: "localStorage" } config, plus generic attribute targeting.
OpenFeature on the webPass-through: whatever you set via OpenFeature.setContext(...) flows into the evaluator, including a manual user: { anonymous: true }.
Node.js SDK, Ruby SDK, Python SDK, Go SDKGeneric attribute targeting: the caller crafts the context (user: { anonymous: true }) and the evaluator reads it through the same path it reads any other attribute. Server-side runtimes don’t need a persistence helper.

The SDK sends nothing per evaluation. The whole context stays on your machine; evaluation runs locally against the datafile. The dashboard never sees individual user contexts unless you opt in to event reporting (which feat does not require).

That property is why feat does not retain a “contexts list” the way some platforms do: there is no inventory of users to leak.

The context shape lines up with the OpenFeature evaluation context. If you write your code against OpenFeature, the same context you pass through OpenFeature.setContext(...) works with the feat Provider unchanged.