OpenFeature
OpenFeature is the CNCF open standard for feature flagging. Featureflip ships providers for the OpenFeature Node.js, .NET, Python, Go, Java, PHP and Ruby server SDKs, so you can use the vendor-neutral OpenFeature API while Featureflip serves your flags. The Node.js provider is covered first; the .NET, Python, Go, Java, PHP and Ruby sections follow.
Installation
Section titled “Installation”npm install @openfeature/server-sdk @featureflip/node @featureflip/openfeature-nodeBoth @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).
Quickstart
Section titled “Quickstart”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.
Context mapping
Section titled “Context mapping”| OpenFeature context | Featureflip context |
|---|---|
targetingKey | user_id (used for rollout bucketing) |
| Any other attribute | Passed through unchanged |
An explicit user_id (or userId) attribute takes precedence over
targetingKey. Without either, percentage rollouts bucketed by user serve the
control variation — see the SDK’s keyless-context behavior.
Evaluation reasons
Section titled “Evaluation reasons”| Featureflip outcome | OpenFeature reason | errorCode |
|---|---|---|
| Targeting rule matched | TARGETING_MATCH | — |
| Fallthrough serve | DEFAULT | — |
| Flag disabled in environment | DISABLED | — |
| Prerequisite not met | PREREQUISITE_FAILED | — |
| Flag not found | ERROR | FLAG_NOT_FOUND |
| Wrong value type requested | ERROR | TYPE_MISMATCH |
| Evaluation error | ERROR | GENERAL |
The matched variation key is exposed as variant, and rule/prerequisite
details are available in flagMetadata (ruleId, prerequisiteKey).
Tracking
Section titled “Tracking”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.
Installation
Section titled “Installation”dotnet add package OpenFeaturedotnet add package Featureflip.Clientdotnet add package Featureflip.OpenFeatureQuickstart
Section titled “Quickstart”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).
.NET limitations
Section titled “.NET limitations”- 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.
Python
Section titled “Python”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.
Installation
Section titled “Installation”pip install featureflip-openfeature-providerQuickstart
Section titled “Quickstart”from openfeature import apifrom openfeature.evaluation_context import EvaluationContextfrom 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 types
Section titled “Python types”Python splits OpenFeature’s numeric accessor into get_integer_value and
get_float_value, which Node does not have:
get_integer_valueaccepts integers and whole-number floats (1.0,1e2). JSON does not distinguish1from1.0, so neither can the guard.get_float_valueaccepts any number, including integral ones.- Both reject booleans.
boolis a subclass ofintin Python, so without an explicit exclusion a boolean flag would satisfyget_integer_valueand return1. get_object_valueaccepts objects and arrays but not strings, even though astris aSequencein 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.
Installation
Section titled “Installation”go get github.com/canopy-labs/featureflip-go-openfeatureQuickstart
Section titled “Quickstart”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")Go types
Section titled “Go types”The OpenFeature Go client spells its boolean read twice, and the difference matters:
client.Boolean(ctx, key, default, evalCtx)returns a plainbool.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.
Installation
Section titled “Installation”dependencies { implementation 'io.featureflip:featureflip-openfeature:0.1.0'}Or with Maven:
<dependency> <groupId>io.featureflip</groupId> <artifactId>featureflip-openfeature</artifactId> <version>0.1.0</version></dependency>Both SDKs come along as transitive dependencies. Java 11 or higher.
Quickstart
Section titled “Quickstart”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.
Java types
Section titled “Java types”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.
Installation
Section titled “Installation”composer require featureflip/featureflip-openfeatureQuickstart
Section titled “Quickstart”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.
PHP types
Section titled “PHP types”resolveIntegerValueaccepts integers and whole-number floats (1.0), since JSON does not distinguish them. A matched value is narrowed to a realintbefore being returned, because the OpenFeature client validates the resolved value’s type and would otherwise return your default with reasonERRORand error codeGENERAL— silently, rather than raising.resolveFloatValueaccepts any JSON number, integers included.resolveObjectValueaccepts objects and arrays only — not strings, even though astris 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 aDateTimeobject left unconverted would stringify unpredictably (or not at all) and silently match no date rule.
PHP limitations
Section titled “PHP limitations”- No provider events. The
open-feature/sdkPHP package has no provider events API at all — noPROVIDER_READY/PROVIDER_CONFIGURATION_CHANGEDequivalent to subscribe to, unlike the other six 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, soruleIdandprerequisiteKeyare not exposed.
Requires featureflip/featureflip-php 3.3.0 or later.
The featureflip-openfeature gem connects the OpenFeature Ruby SDK to
Featureflip, backed by the featureflip server SDK.
Installation
Section titled “Installation”# Gemfilegem "featureflip-openfeature"Quickstart
Section titled “Quickstart”require "featureflip-openfeature"
OpenFeature::SDK.set_provider_and_wait( Featureflip::OpenFeature::Provider.new(sdk_key: "your-server-sdk-key"))
client = OpenFeature::SDK.build_clientenabled = 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.
Ruby types
Section titled “Ruby types”fetch_integer_valueaccepts whole-number floats and returns anInteger(3.0comes back as3). Past 2⁵³ a float can no longer stand for one exact integer, so those resolve asTYPE_MISMATCH.fetch_float_valueaccepts integers and returns aFloat.fetch_object_valueaccepts hashes and arrays only.Time,DateandDateTimeattributes become ISO-8601 strings, nested ones included, which is the form date rules compare against.
Ruby requirements
Section titled “Ruby requirements”The provider needs featureflip 2.10.0 or newer, the release that added the
update hook, 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.
Reacting to flag changes
Section titled “Reacting to flag changes”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 apifrom 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}" })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.
Limitations
Section titled “Limitations”getObjectValueaccepts objects and arrays only: a Json flag holding a bare primitive (string/number/boolean) resolves asTYPE_MISMATCHand returns the default.- Node.js, .NET, Python, Go, Java, PHP and Ruby have OpenFeature providers; other SDKs are in progress.
- The PHP provider has no provider-events API to subscribe to (the
open-feature/sdkPHP package doesn’t expose one), so flag changes there arrive on the underlying SDK’s poll interval rather than being pushed to your application.