Skip to content

OpenFeature

OpenFeature is the CNCF open standard for feature flagging. Featureflip ships providers for the OpenFeature Node.js, .NET, Python, Go, Java, PHP, Ruby, Web and Android SDKs, so you can use the vendor-neutral OpenFeature API while Featureflip serves your flags. The Node.js provider is covered first, and the .NET, Python, Go, Java, PHP, Ruby, Web and React and Android sections follow.

Terminal window
npm install @openfeature/server-sdk @featureflip/node @featureflip/openfeature-node

Both @openfeature/server-sdk and @featureflip/node are peer dependencies — your application controls their versions and installs a single copy of each. A single @featureflip/node copy is what lets the provider and any direct SDK usage share one underlying client core (see below).

import { OpenFeature } from '@openfeature/server-sdk';
import { FeatureflipProvider } from '@featureflip/openfeature-node';
await OpenFeature.setProviderAndWait(
new FeatureflipProvider({ sdkKey: 'your-server-sdk-key' }),
);
const client = OpenFeature.getClient();
const enabled = await client.getBooleanValue(
'new-checkout',
false,
{ targetingKey: 'user-42', plan: 'pro' },
);

The provider owns the underlying Featureflip client. If you also need the raw SDK (for track() calls outside OpenFeature, for example), call FeatureflipClient.get() with the same SDK key — both handles share one client core. You can also pass an existing FeatureflipClient instance to the provider constructor instead of a config.

OpenFeature contextFeatureflip context
targetingKeyuser_id (used for rollout bucketing)
Any other attributePassed through unchanged

An explicit user_id (or userId) attribute takes precedence over targetingKey. Without either, percentage rollouts bucketed by user skip that caller, who gets the next matching rule or the default variation. Server SDKs log a warning once per flag and rule.

Featureflip outcomeOpenFeature reasonerrorCode
Targeting rule matchedTARGETING_MATCH—
Fallthrough serveDEFAULT—
Flag disabled in environmentDISABLED—
Prerequisite not metPREREQUISITE_FAILED—
Flag not foundERRORFLAG_NOT_FOUND
Wrong value type requestedERRORTYPE_MISMATCH
Evaluation errorERRORGENERAL

The matched variation key is exposed as variant, and rule/prerequisite details are available in flagMetadata (ruleId, prerequisiteKey).

OpenFeature tracking calls are forwarded to Featureflip custom events:

client.track('purchase', { targetingKey: 'user-42' }, { value: 9.99 });

The Featureflip.OpenFeature NuGet package connects the OpenFeature .NET SDK to Featureflip, backed by the Featureflip.Client server SDK.

Terminal window
dotnet add package OpenFeature
dotnet add package Featureflip.Client
dotnet add package Featureflip.OpenFeature
using OpenFeature;
using Featureflip.OpenFeature;
await Api.Instance.SetProviderAsync(new FeatureflipProvider("your-server-sdk-key"));
var client = Api.Instance.GetClient();
var enabled = await client.GetBooleanValueAsync("new-checkout", false,
EvaluationContext.Builder().SetTargetingKey("user-42").Build());

The constructor accepts a server SDK key (with optional FeatureFlagOptions) or an existing IFeatureflipClient. Context mapping and reasons match the Node provider (targetingKey → user_id; Variant, ruleId/prerequisiteKey in FlagMetadata; PREREQUISITE_FAILED for unmet prerequisites).

  • The .NET SDK has no custom-event API, so OpenFeature Track() is a no-op.
  • Object flags accept objects and arrays only; a bare primitive resolves as TYPE_MISMATCH.

The featureflip-openfeature-provider package connects the OpenFeature Python SDK to Featureflip, backed by the featureflip server SDK. New to that SDK? The Python quickstart sets it up in a few minutes, and feature flags in Python covers where to build the client under a forking web server.

Terminal window
pip install featureflip-openfeature-provider
from openfeature import api
from openfeature.evaluation_context import EvaluationContext
from featureflip_openfeature import FeatureflipProvider
api.set_provider_and_wait(FeatureflipProvider(sdk_key="your-server-sdk-key"))
client = api.get_client()
enabled = client.get_boolean_value(
"new-checkout",
False,
EvaluationContext(targeting_key="user-42", attributes={"plan": "pro"}),
)

The constructor takes either an SDK key (with optional Config) or an existing FeatureflipClient via the client= keyword. Context mapping and reasons match the Node provider. targeting_key becomes user_id, the matched variation is exposed as variant, ruleId and prerequisiteKey are carried in flag_metadata, and an unmet prerequisite resolves as PREREQUISITE_FAILED.

Ownership follows the .NET provider rather than Node: the provider closes the client only when it created one. A client you pass in is a refcounted handle you still hold, so closing it would make your own handle start returning defaults.

FeatureflipClient(...) blocks on the initial flag fetch. With the sdk_key form the provider defers construction to initialize(), so that wait happens inside OpenFeature’s provider setup rather than in your constructor.

Reach for set_provider_and_wait rather than set_provider. The plain form runs initialize() on a background thread and returns straight away, so flags may not have loaded when the next line evaluates one and you get your default back. This is the Python equivalent of awaiting setProviderAndWait in Node.

Python splits OpenFeature’s numeric accessor into get_integer_value and get_float_value, which Node does not have:

  • get_integer_value accepts integers and whole-number floats (1.0, 1e2). JSON does not distinguish 1 from 1.0, so neither can the guard.
  • get_float_value accepts any number, including integral ones.
  • Both reject booleans. bool is a subclass of int in Python, so without an explicit exclusion a boolean flag would satisfy get_integer_value and return 1.
  • get_object_value accepts objects and arrays but not strings, even though a str is a Sequence in Python.

Configuration-change events need featureflip >= 2.7.0, the release that added the SDK’s update hook.

The github.com/canopy-labs/featureflip-go-openfeature module connects the OpenFeature Go SDK to Featureflip, backed by the featureflip-go server SDK. Starting from nothing, the Go quickstart gets that SDK evaluating a flag first.

Terminal window
go get github.com/canopy-labs/featureflip-go-openfeature
import (
featureflip "github.com/canopy-labs/featureflip-go-openfeature"
"github.com/open-feature/go-sdk/openfeature"
)
if err := openfeature.SetProviderAndWait(featureflip.NewProvider("your-server-sdk-key")); err != nil {
log.Fatal(err)
}
client := openfeature.NewClient("my-app")
enabled, err := client.BooleanValue(ctx, "new-checkout", false,
openfeature.NewEvaluationContext("user-42", map[string]any{"plan": "pro"}))

NewProvider takes an SDK key plus any Featureflip SDK options; NewProviderWithClient takes an existing client instead. Ownership follows the .NET and Python providers: Shutdown closes the client only when the provider created it.

Reach for SetProviderAndWait rather than SetProvider, for the same reason as in Node and Python — the plain form initializes on a background goroutine and returns before flags have loaded.

If you import the Featureflip SDK directly as well, alias one of them. Both packages are named featureflip:

import (
sdk "github.com/canopy-labs/featureflip-go/v2"
featureflip "github.com/canopy-labs/featureflip-go-openfeature"
)

The OpenFeature Go client spells its boolean read twice, and the difference matters:

  • client.Boolean(ctx, key, default, evalCtx) returns a plain bool.
  • client.BooleanValue(ctx, key, default, evalCtx) returns (bool, error).

Both resolve identically through the provider. The two-value form surfaces the resolution error; the single-value form swallows it and hands back your default.

IntEvaluation accepts whole-number JSON values in either form (1 and 1.0), since JSON does not distinguish them. Object flags accept objects and arrays only, so a Json flag holding a bare primitive resolves as TYPE_MISMATCH.

The Featureflip Go SDK resolves the attribute names userId and user_id from its own user-id field rather than from the attribute map, so the targeting key and both attribute spellings are lifted into it. An explicit user_id or userId wins over targetingKey.

The io.featureflip:featureflip-openfeature artifact connects the OpenFeature Java SDK to Featureflip, backed by the featureflip-java server SDK.

dependencies {
implementation 'io.featureflip:featureflip-openfeature:0.2.1'
}

Or with Maven:

<dependency>
<groupId>io.featureflip</groupId>
<artifactId>featureflip-openfeature</artifactId>
<version>0.2.1</version>
</dependency>

Both SDKs come along as transitive dependencies. Java 11 or higher.

import dev.openfeature.sdk.*;
import io.featureflip.openfeature.FeatureflipProvider;
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProviderAndWait(new FeatureflipProvider("your-server-sdk-key"));
Client client = api.getClient();
boolean enabled = client.getBooleanValue("new-checkout", false,
new ImmutableContext("user-42", Map.of("plan", new Value("pro"))));

The constructor takes an SDK key, optionally with a FeatureFlagConfig; a third form takes an existing FeatureflipClient. Ownership follows the .NET, Python and Go providers: shutdown() closes the client only when the provider created it.

Reach for setProviderAndWait rather than setProvider, for the same reason as in the other runtimes — the plain form returns before flags have loaded.

getIntegerEvaluation accepts whole-number JSON values in either form (1 and 1.0), since JSON does not distinguish them. Object flags accept objects and arrays only, so a Json flag holding a bare primitive resolves as TYPE_MISMATCH.

A wrong-typed flag is reported as TYPE_MISMATCH distinctly from a genuine evaluation failure, which is GENERAL. The provider reads values untyped and applies the type check itself to keep those apart — the SDK’s typed accessors report both as an error.

The Featureflip Java SDK resolves the attribute names userId and user_id from its own user-id field rather than from the attribute map, so the targeting key and both attribute spellings are lifted into it. An explicit user_id or userId wins over targetingKey. A numeric identity is rendered without a decimal part, so an id sent as a number buckets identically to the same id sent as a string.

A failed initial flag load does not fail setProviderAndWait. The SDK keeps retrying in the background and evaluations serve your defaults until flags arrive, so a transient outage at startup degrades rather than takes the provider down.

The featureflip/featureflip-openfeature Packagist package connects the OpenFeature PHP SDK to Featureflip, backed by the featureflip/featureflip-php server SDK.

Terminal window
composer require featureflip/featureflip-openfeature
use Featureflip\FeatureflipClient;
use Featureflip\Config;
use Featureflip\OpenFeature\FeatureflipProvider;
use OpenFeature\OpenFeatureAPI;
$client = FeatureflipClient::get('your-server-sdk-key', new Config(
cache: $psrCache,
httpClient: $psrHttpClient,
requestFactory: $psrRequestFactory,
streamFactory: $psrStreamFactory,
));
OpenFeatureAPI::getInstance()->setProvider(new FeatureflipProvider($client));
$enabled = OpenFeatureAPI::getInstance()
->getClient()
->getBooleanValue('new-checkout', false, $context);

Unlike the other five providers on this page, which all accept a server SDK key directly, there is no SDK-key constructor here: build a FeatureflipClient yourself and pass it in. The PHP SDK’s core hard-requires caller-supplied PSR-18 (HTTP client), PSR-17 (request/stream factory) and PSR-16 (cache) implementations and throws without them, so a key-only constructor would have to choose an HTTP client and a cache on the application’s behalf. Passing the client in is also the idiomatic PHP shape: it is a service-container object, and an application evaluating flags through both the SDK directly and through OpenFeature should share one. The provider never closes the client it is given; the caller owns it and is responsible for calling $client->close() at shutdown.

Reasons and variants match the other providers: the matched variation is exposed as variant, and an unmet prerequisite resolves as PREREQUISITE_FAILED. The targeting key is written into the Featureflip context under both user_id and userId, because the PHP SDK resolves a condition’s attribute by exact key with no aliasing between the two spellings — writing both is what lets a targeting rule against either one resolve. An explicit user_id or userId attribute still takes precedence and is passed through exactly as you supplied it.

  • resolveIntegerValue accepts integers and whole-number floats (1.0), since JSON does not distinguish them. A matched value is narrowed to a real int before being returned, because the OpenFeature client validates the resolved value’s type and would otherwise return your default with reason ERROR and error code GENERAL — silently, rather than raising.
  • resolveFloatValue accepts any JSON number, integers included.
  • resolveObjectValue accepts objects and arrays only — not strings, even though a str is treated as a sequence in some SDKs’ equivalent accessor.
  • A DateTime-valued context attribute is converted to an ISO-8601 string before evaluation, recursing into nested arrays. Every SDK gates its date-comparison rules on the ISO grammar, so a DateTime object left unconverted would stringify unpredictably (or not at all) and silently match no date rule.
  • No provider events. The open-feature/sdk PHP package has no provider events API at all — no PROVIDER_READY/PROVIDER_CONFIGURATION_CHANGED equivalent to subscribe to, unlike the other eight providers on this page. Flag changes are therefore picked up passively, on the underlying SDK’s poll interval, rather than pushed to your application.
  • No flag metadata at this version. The released open-feature/sdk (2.3.0) has no flag-metadata API, so ruleId and prerequisiteKey are not exposed.

Requires featureflip/featureflip-php 3.7.0 or later.

The featureflip-openfeature gem connects the OpenFeature Ruby SDK to Featureflip, backed by the featureflip server SDK.

# Gemfile
gem "featureflip-openfeature"
require "featureflip-openfeature"
OpenFeature::SDK.set_provider_and_wait(
Featureflip::OpenFeature::Provider.new(sdk_key: "your-server-sdk-key")
)
client = OpenFeature::SDK.build_client
enabled = client.fetch_boolean_value(
flag_key: "new-checkout",
default_value: false,
evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "user-42", plan: "pro")
)

The constructor takes an SDK key (with an optional Featureflip::Config) or an existing client through client:. The provider closes a client only when it built one. Pass in your own and it stays yours.

Featureflip::Client.get waits for the first flag fetch, so with the sdk_key: form the provider builds the client when OpenFeature initializes it. Use set_provider_and_wait. If the first fetch fails or times out, it raises OpenFeature::SDK::ProviderInitializationError instead of returning a provider that serves defaults.

Reasons, variants and metadata match the other providers: ruleId and prerequisiteKey sit in flag_metadata, and an unmet prerequisite resolves as PREREQUISITE_FAILED. The targeting key is written under both user_id and userId, so a rule on either spelling matches. An explicit attribute of either spelling is passed through as you supplied it.

  • fetch_integer_value accepts whole-number floats and returns an Integer (3.0 comes back as 3). Past 2⁵³ a float can no longer stand for one exact integer, so those resolve as TYPE_MISMATCH.
  • fetch_float_value accepts integers and returns a Float.
  • fetch_object_value accepts hashes and arrays only.
  • Time, Date and DateTime attributes become ISO-8601 strings, nested ones included, which is the form date rules compare against.

The provider needs featureflip 2.13.0 or newer, and openfeature-sdk 0.5 or newer but older than 0.7. The 0.6 line needs Ruby 3.4 or newer. Tracking calls reach Featureflip only on openfeature-sdk 0.6.1 or newer, the release that added the tracking API.

On a forking server (Puma with preload_app!, Unicorn, Passenger), register the provider in the post-fork hook (on_worker_boot or after_fork). A provider built before the fork leaves each worker with dead polling and streaming threads, so it serves the boot snapshot and never emits PROVIDER_CONFIGURATION_CHANGED. If you pass your own client:, call client.restart in that hook instead, as the Ruby SDK guide describes.

The SDK keeps one client core per SDK key in each process. So if your app also builds a client for the same key before the fork, say with Featureflip.configure in a Rails initializer, the provider registered after the fork picks up that inherited core, dead threads and all. Call Featureflip.restart in the same hook to bring it back to life.

The @featureflip/openfeature-web package plugs Featureflip into the OpenFeature web SDK, on top of the @featureflip/browser client. Browser apps use it directly, and React apps reach it through @openfeature/react-sdk.

Terminal window
npm install @openfeature/web-sdk @featureflip/browser @featureflip/openfeature-web
import { OpenFeature } from '@openfeature/web-sdk';
import { FeatureflipProvider } from '@featureflip/openfeature-web';
await OpenFeature.setProviderAndWait(
new FeatureflipProvider({ clientKey: 'your-client-sdk-key' }),
{ targetingKey: 'user-42', plan: 'pro' },
);
const client = OpenFeature.getClient();
const enabled = client.getBooleanValue('new-checkout', false);
// When the user changes:
await OpenFeature.setContext({ targetingKey: 'user-43', plan: 'free' });

You hand the web SDK a user once, when the provider starts, and again whenever setContext changes it, and both times the provider fetches that user’s flags before the promise resolves. Reads never wait on the network. getBooleanValue returns a plain value from flags the browser already holds. With bootstrap values in the config the provider is ready at once, and the server’s flags replace them when they arrive.

Use a client-side SDK key, since only flags marked client-side visible ever reach the browser. Any other key resolves as FLAG_NOT_FOUND.

Pass the constructor a config object or an existing FeatureflipClient. With a config, the client is created when OpenFeature initializes the provider, which means the very first request already carries your context instead of an empty one that gets replaced a moment later. Any context in the config survives. Where it and the OpenFeature context set the same attribute, OpenFeature’s value wins.

The browser SDK keeps one client per client key, and it serves one user at a time. So if your app also reads flags through @featureflip/browser directly with the same key, an identify() there re-targets the provider too. Change the user in one place, ideally setContext.

import { OpenFeature, OpenFeatureProvider, useBooleanFlagValue } from '@openfeature/react-sdk';
import { FeatureflipProvider } from '@featureflip/openfeature-web';
OpenFeature.setProvider(new FeatureflipProvider({ clientKey: 'your-client-sdk-key' }));
function App() {
return (
<OpenFeatureProvider>
<Checkout />
</OpenFeatureProvider>
);
}
function Checkout() {
const newCheckout = useBooleanFlagValue('new-checkout', false);
return newCheckout ? <NewCheckout /> : <OldCheckout />;
}

Values served before the server answers, from bootstrap or from localStorage, report CACHED. The rest matches the table above, with ruleId and prerequisiteKey in flagMetadata and an unmet prerequisite resolving as PREREQUISITE_FAILED.

  • track() is a no-op. The browser SDK has no custom-event API to forward it to.
  • Flag cleanup rewrites getBooleanValue reads in files that import @openfeature/web-sdk and no other @openfeature package. React files import @openfeature/react-sdk, so their reads, useBooleanFlagValue and the other hooks included, need removing by hand when you retire a flag.

The io.featureflip:featureflip-android-openfeature package connects the OpenFeature Kotlin SDK to Featureflip, backed by the Featureflip Android SDK.

dependencies {
implementation("io.featureflip:featureflip-android-openfeature:0.1.2")
}

The Featureflip Android SDK and the OpenFeature Kotlin SDK come along transitively, so you don’t declare either one. It needs Java 17 or higher.

Set the provider once per process, not per Activity. The Application class is the natural home, with a coroutine scope that lives as long as the app does:

import android.app.Application
import dev.featureflip.android.FeatureflipConfig
import dev.featureflip.android.openfeature.FeatureflipProvider
import dev.openfeature.kotlin.sdk.ImmutableContext
import dev.openfeature.kotlin.sdk.OpenFeatureAPI
import dev.openfeature.kotlin.sdk.Value
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class MyApp : Application() {
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
override fun onCreate() {
super.onCreate()
appScope.launch {
OpenFeatureAPI.setProviderAndWait(
FeatureflipProvider(
FeatureflipConfig(
clientKey = "your-client-sdk-key",
applicationContext = applicationContext,
),
),
initialContext = ImmutableContext(
targetingKey = "user-42",
attributes = mapOf("plan" to Value.String("pro")),
),
)
// The provider is ready here, so this reads the real value.
val enabled = OpenFeatureAPI.getClient().getBooleanValue("new-checkout", false)
}
}
}
// When the user changes, from any coroutine:
OpenFeatureAPI.setEvaluationContextAndWait(ImmutableContext(targetingKey = "user-43"))

An Activity is the wrong place. Rotating the screen recreates it and setup runs again, so a fresh provider replaces the old one and fetches every flag a second time while the old one shuts down.

setProviderAndWait returns once the provider holds flags for that first user, so a read after it in the same coroutine gets the real value. A screen that reads before setup is done gets the default, so if your first screen needs a flag, wait for OpenFeatureAPI.statusFlow to report Ready. Plain setProvider returns before initialization finishes, which means a read on the next line can come back as its default.

Switching users works the same way. setEvaluationContextAndWait returns once the new user’s flags have arrived. The plain setEvaluationContext returns at once and fetches in the background, and until that finishes, reads keep serving the previous user’s flags. Reads never wait on the network either way. getBooleanValue looks the value up in flags the app already holds, which were evaluated for the user you set. Reads take no context of their own.

Passing applicationContext lets the SDK save the id it generates for an anonymous user. Without it that id changes on every app restart, and the user can land in a different rollout bucket each time.

Use a client-side SDK key, since only flags marked client-side visible reach the app. Any other key resolves as FLAG_NOT_FOUND.

The constructor takes a FeatureflipConfig or an existing FeatureflipClient. With a config, the provider builds the client when OpenFeature initializes it, so the first request already carries your user. With a client, shutdown() closes the handle you passed. Give the provider a handle of its own, not the one the rest of your app uses.

One caveat applies when the app already created a client for the same key. The SDK keeps one client per key and ignores a second config, so the provider detects that case and applies your user with identify instead. A few races can’t be detected, such as two threads making the first get for a new key at once, and there the client can end up on the other caller’s context.

The SDK carries a single wrapper name. If your config sets wrapperName, the provider keeps yours and adds none, and otherwise requests identify it as featureflip-openfeature-android.

The reason mapping is the one in the table above, except that nothing resolves as CACHED. An Android client answers every read from its cache, so the SDK keeps no record of where a value came from, and calling the normal case a degraded one would mislead. Reasons describe how the server evaluated the flag. ruleId and prerequisiteKey appear in flagMetadata when they apply, and an unmet prerequisite resolves as PREREQUISITE_FAILED.

A flag of the wrong type resolves to your default with TYPE_MISMATCH.

  • Integer reads accept a whole number within Int range, whether the JSON wrote it as 1 or 1.0.
  • Long reads accept a whole number within Long range. A larger integer, or a value like 1e30, is a TYPE_MISMATCH.
  • Double reads accept any number.
  • Object reads accept objects and arrays only.

Value.Instant attributes in a context or in tracking details are sent as ISO-8601 strings.

Setting the user is serialized. Each set finishes its identify before the next one starts, and the newest context always lands, even if the caller that asked for it was cancelled. A failed identify leaves the previous user’s flags serving. A failed first load doesn’t fail initialization either. The SDK keeps retrying, and reads serve flags cached from the last session, or your defaults, until the real ones arrive.

client.track() calls are forwarded to Featureflip custom events.

client.track(
"checkout-completed",
TrackingEventDetails(99.99, ImmutableStructure("currency" to Value.String("USD"))),
)

Identify and initialize block inside the SDK, so the provider runs them on Dispatchers.IO. Flag reads are cache lookups and run on the calling thread.

  • Flag cleanup rewrites getBooleanValue reads in Kotlin files that import something from dev.openfeature. A file that reads the flag that way without such an import makes the run skip the flag and name the file. getBooleanDetails, the non-boolean accessors and safe calls like client?.getBooleanValue(…) are left as they are, so remove those by hand when you retire a flag.

The Node provider emits OpenFeature’s PROVIDER_CONFIGURATION_CHANGED whenever flag configuration changes after startup. The event carries flagsChanged — the keys of the flags affected by that change, batched into a single event:

import { OpenFeature, ProviderEvents } from "@openfeature/server-sdk";
OpenFeature.addHandler(ProviderEvents.ConfigurationChanged, (details) => {
console.log("flags changed:", details?.flagsChanged);
});

The .NET provider emits the same event, from version 0.2.0:

using OpenFeature;
using OpenFeature.Constant;
Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, details =>
{
Console.WriteLine($"flags changed: {string.Join(", ", details.FlagsChanged ?? [])}");
});

The Python provider emits it too:

from openfeature import api
from openfeature.event import ProviderEvent
api.add_handler(
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED,
lambda details: print("flags changed:", details.flags_changed),
)

The Go provider emits it too, from version 0.2.0:

callback := func(details openfeature.EventDetails) {
log.Printf("flags changed: %v", details.FlagChanges)
}
openfeature.AddHandler(openfeature.ProviderConfigChange, &callback)

The Java provider emits it too:

client.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details ->
System.out.println("flags changed: " + details.getFlagsChanged()));

The Ruby provider emits it too:

OpenFeature::SDK.add_handler(
OpenFeature::SDK::ProviderEvent::PROVIDER_CONFIGURATION_CHANGED,
->(details) { puts "flags changed: #{details[:flags_changed].inspect}" }
)

The web provider emits it too:

import { OpenFeature, ProviderEvents } from '@openfeature/web-sdk';
OpenFeature.addHandler(ProviderEvents.ConfigurationChanged, (details) => {
console.log('flags changed:', details?.flagsChanged);
});

The browser holds evaluated values for one user, so the web provider reports the flags whose value for that user changed. Switching users with setContext raises PROVIDER_CONTEXT_CHANGED and skips this event.

The Android provider emits it too:

import dev.openfeature.kotlin.sdk.OpenFeatureAPI
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents
OpenFeatureAPI.observe<OpenFeatureProviderEvents.ProviderConfigurationChanged>()
.collect { println("flags changed: ${it.eventDetails?.flagsChanged}") }

The Android client holds evaluated values for one user as well. Switching users with setEvaluationContext doesn’t raise this event. The Kotlin SDK has no PROVIDER_CONTEXT_CHANGED, so the status goes from Reconciling to Ready instead, and you can observe the status flow to know when the new user’s flags are in.

A flag is reported when it is created, deleted, redefined, or when a segment its targeting rules reference changes. A flag is also reported when a flag it lists as a prerequisite changes, since that alters what it evaluates to. The initial flag load does not fire the event — that is signaled by PROVIDER_READY. The one exception is the web provider started from bootstrap values: the server’s first answer fires it for every flag whose value that answer changed.

  • getObjectValue accepts objects and arrays only: a Json flag holding a bare primitive (string/number/boolean) resolves as TYPE_MISMATCH and returns the default.
  • Node.js, .NET, Python, Go, Java, PHP, Ruby, Web and Android have OpenFeature providers.
  • The PHP provider has no provider-events API to subscribe to (the open-feature/sdk PHP package doesn’t expose one), so flag changes there arrive on the underlying SDK’s poll interval rather than being pushed to your application.