Skip to content

OpenFeature

OpenFeature is a CNCF specification for a vendor-neutral feature flag API. feat ships providers for the OpenFeature web SDK, the OpenFeature Node.js SDK, and the OpenFeature Dart SDK. If you write your code against OpenFeature, you can swap feat for any other OpenFeature-compatible vendor without changing application code.

feat is listed in the OpenFeature ecosystem.

Two practical reasons:

  • Vendor portability. Your application code calls OpenFeature.getClient().getBooleanValue(...). Swapping the provider is a one-line change. The rest of your app does not care.
  • Standard surface. Engineers familiar with OpenFeature do not have to learn a new client API.

Spec reference: openfeature.dev/specification.

The FeatProvider class is bundled with @feathq/js-sdk. No extra install required.

import { OpenFeature } from "@openfeature/server-sdk";
import { FeatClient, FeatProvider } from "@feathq/js-sdk";
const featClient = new FeatClient({
apiKey: process.env.FEAT_SERVER_KEY!,
url: "https://data-01.feat.so", // optional; this is the default
});
await OpenFeature.setProviderAndWait(new FeatProvider(featClient));
const client = OpenFeature.getClient();
const enabled = await client.getBooleanValue("checkout-v2", false, {
targetingKey: "user-123",
plan: "pro",
});

See Node.js SDK for everything else the client supports.

The web provider is a separate package because the OpenFeature web spec uses the static-context paradigm: the evaluation context lives on the provider, not on each call. The web provider routes context through OpenFeature.setContext.

Terminal window
npm install @feathq/web-sdk @feathq/openfeature-web @openfeature/web-sdk
import { OpenFeature } from "@openfeature/web-sdk";
import { FeatWebClient } from "@feathq/web-sdk";
import { FeatWebProvider } from "@feathq/openfeature-web";
const featClient = new FeatWebClient({
apiKey: "feat_cs_…",
url: "https://data-01.feat.so", // optional; this is the default
});
await OpenFeature.setProviderAndWait(new FeatWebProvider(featClient));
await OpenFeature.setContext({ targetingKey: "user-123" });
const client = OpenFeature.getClient();
const enabled = client.getBooleanValue("checkout-v2", false); // sync

@feathq/web-sdk and @openfeature/web-sdk are peer dependencies. Use OpenFeature.setContext (not the per-call context) for the web provider; it routes through to client.setContext on the underlying feat client.

The web provider forwards feat’s live flag changes to OpenFeature as ProviderEvents.ConfigurationChanged. That means the OpenFeature React SDK hooks re-render automatically when a flag or the context changes, no manual wiring:

import { OpenFeatureProvider, useBooleanFlagValue } from "@openfeature/react-sdk";
function Checkout() {
const enabled = useBooleanFlagValue("checkout-v2", false);
return enabled ? <CheckoutV2 /> : <CheckoutV1 />;
}
// Wrap your tree once:
<OpenFeatureProvider>
<Checkout />
</OpenFeatureProvider>;

For server-rendered React (Next.js App Router, Remix), seed the feat client with a server-evaluated snapshot via bootstrap so the first render is correct. See Server-side rendering.

The Dart provider wraps the feat Dart SDK to expose feat through the OpenFeature Dart server SDK’s FeatureProvider interface.

dependencies:
feat_sdk: ^0.1.0
feat_openfeature: ^0.1.0
openfeature_dart_server_sdk: ^0.0.22
import 'package:feat_openfeature/feat_openfeature.dart';
import 'package:feat_sdk/feat_sdk.dart';
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';
final featClient = await FeatClient.initialize(
FeatClientConfig(
apiKey: 'feat_cs_...', // client-side ID or mobile key, never a server key
context: EvalContext.user('user-123'),
),
);
final api = OpenFeatureAPI();
await api.setProviderAndWait(FeatProvider(featClient));
final client = api.getClient('my-app');
final enabled = await client.getBooleanFlag('checkout-v2', defaultValue: false);

Note: this is a server-paradigm provider (async signatures) wrapping a static-context client, so the per-call context argument is ignored. Change context with FeatProvider.setContext(...) / setEvaluationContext(...), which re-evaluates and refreshes the cache. See Dart SDK.

The provider returns the supplied default value with an errorCode of TYPE_MISMATCH if you ask a boolean flag for a string, etc. When a flag cannot be evaluated it returns the default with reason: "ERROR" and an errorCode. The web provider maps:

SituationerrorCode
Wrong type requestedTYPE_MISMATCH
Client not ready (no context yet)PROVIDER_NOT_READY
Flag does not existFLAG_NOT_FOUND
Any other evaluation errorGENERAL

The Dart provider emits FLAG_NOT_FOUND and TYPE_MISMATCH from the same rules; it does not surface PROVIDER_NOT_READY or GENERAL.

For a successful evaluation the provider passes feat’s native reason straight through, unmapped: TARGETING_MATCH, SPLIT, FALLTHROUGH, DEFAULT, DISABLED, or STATIC. See Evaluation order for what each means. (OpenFeature’s reason field is a free-form string, so feat’s vocabulary carries through as-is; errorCode is only set on failures.)

The Go, Python, and Ruby SDKs expose the feat-native API documented in their per-language pages. A provider for each is planned; use the native API in the meantime.