Note
The repository is still under development. The NPM package is not yet published.
OpenFeature provider for Flagr feature flagging service.
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagrProvider } from '@openflagr/flagr-openfeature-provider-js';
// Initialize the provider
OpenFeature.setProvider(
new FlagrProvider({
baseUrl: 'http://localhost:18000',
truthyVariants: new Set(['on', 'true', 'enabled']),
})
);
// Get a client and evaluate flags
const client = OpenFeature.getClient();
const isEnabled = await client.getBooleanValue('my-feature', false, {
targetingKey: 'user-123',
});| Option | Type | Required | Default | Description |
|---|---|---|---|---|
baseUrl |
string |
Yes | - | Base URL of the Flagr server (e.g., http://localhost:18000) |
truthyVariants |
Set<string> |
Yes | - | Variant keys that resolve to true for boolean evaluations (case-insensitive) |
timeout |
number |
No | 5000 |
Request timeout in milliseconds |
resolveBooleanEvaluation maps Flagr's variantKey to a boolean value using the truthyVariants configuration.
| variantKey | truthyVariants | Result |
|---|---|---|
"on" |
new Set(["on", "true"]) |
true |
"off" |
new Set(["on", "true"]) |
false |
"ON" |
new Set(["on", "true"]) |
true (case-insensitive) |
"enabled" |
new Set(["on", "true"]) |
false (not in set) |
Returns default value when:
- Flag not found
- No segment matched (empty response)
const isEnabled = await client.getBooleanValue('feature-flag', false);Design Notes:
- Case-insensitive matching:
"ON","on", and"On"are all treated the same - Implicit falsy: Any
variantKeyNOT intruthyVariantsresolves tofalse - No attachment fallback: Unlike number evaluation, boolean does NOT fall back to
variantAttachment.value- the result is determined solely byvariantKeymembership intruthyVariants
resolveStringEvaluation returns the variantKey directly.
| Flagr Response | OpenFeature Result |
|---|---|
variantKey: "variant-a" |
"variant-a" |
variantKey: "control" |
"control" |
| No match | defaultValue |
const variant = await client.getStringValue('experiment-flag', 'control');resolveNumberEvaluation uses a two-tier resolution strategy:
- Tier 1: Parse
variantKeyas a number - Tier 2: Fall back to
variantAttachment.valueif tier 1 fails
| variantKey | variantAttachment | Result |
|---|---|---|
"42" |
{} |
42 |
"control" |
{ value: 100 } |
100 |
"control" |
{} |
defaultValue + PARSE_ERROR |
const limit = await client.getNumberValue('rate-limit', 100);resolveObjectEvaluation returns the variantAttachment directly.
| variantAttachment | Result |
|---|---|
{ theme: "dark", limit: 10 } |
{ theme: "dark", limit: 10 } |
null / missing |
defaultValue |
const config = await client.getObjectValue('feature-config', { theme: 'light' });OpenFeature context values are converted to strings for Flagr compatibility:
| OpenFeature Type | Flagr Conversion |
|---|---|
string |
Direct pass-through |
number |
String(value) |
boolean |
"true" / "false" |
Date |
ISO 8601 string |
object |
JSON.stringify() |
array |
JSON.stringify() |
Special keys:
targetingKey→ FlagrentityID
await client.getBooleanValue('my-flag', false, {
targetingKey: 'user-123', // → entityID
plan: 'premium', // → entityContext.plan = "premium"
age: 25, // → entityContext.age = "25"
isAdmin: true, // → entityContext.isAdmin = "true"
});| Reason | When Used |
|---|---|
TARGETING_MATCH |
Flagr returned a matching segment with variant |
DEFAULT |
No segment matched (Flagr returns empty response) |
ERROR |
Network failure, parse error, or type mismatch |
| ErrorCode | Condition |
|---|---|
FLAG_NOT_FOUND |
Flagr returns 404 or flag doesn't exist |
PARSE_ERROR |
Number evaluation fails to parse variantKey/attachment |
TYPE_MISMATCH |
Number evaluation receives non-number attachment value |
GENERAL |
Network errors, timeouts, unexpected failures |
When a flag evaluation succeeds with a match, the following metadata is returned:
{
segmentID: number; // Matched segment ID from Flagr
flagSnapshotID: number; // Version/snapshot of the flag evaluated
}Access via ResolutionDetails:
const details = await client.getBooleanDetails('my-flag', false);
console.log(details.flagMetadata?.segmentID);The provider performs a health check against Flagr's /api/v1/health endpoint during initialization. If the health check fails, a PROVIDER_ERROR event is emitted.
await OpenFeature.setProviderAndWait(new FlagrProvider(config));
// Provider is ready, health check passedawait OpenFeature.close();
// Provider cleanup complete (no-op for Flagr, no persistent connections)// lib/flags.ts
import { createOpenFeatureAdapter } from '@flags-sdk/openfeature';
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagrProvider } from 'openflagr/flagr-openfeature-provider-js';
OpenFeature.setProvider(
new FlagrProvider({
baseUrl: process.env.FLAGR_URL!,
truthyVariants: new Set(['on', 'true', 'enabled']),
})
);
export const flagrAdapter = createOpenFeatureAdapter(OpenFeature.getClient());// page.ts
import {flag} from 'flags/next';
import {flagrAdapter} from './lib/flags';
const EXAMPLE_BOOLEAN_FLAG_KEY = 'example-boolean-flag';
export const getServerSideProps = (async ({req}) => {
const exampleFlag = await featureClient.getBooleanValue(EXAMPLE_BOOLEAN_FLAG_KEY, false);
return {props: {example}};
}) satisfies GetServerSideProps<{ example: boolean }>;
export default async function Page() {
return (
<>
Exampleflag is {exampleFlag : "enabled":"disabled"}
</>
);
}
// app/page.tsx (Server Component)
import { OpenFeature } from '@openfeature/server-sdk';
export default async function Page() {
const client = OpenFeature.getClient();
const showBanner = await client.getBooleanValue('show-banner', false, {
targetingKey: 'anonymous',
});
return showBanner ? <Banner /> : null;
}- Server-only: This provider uses remote evaluation (HTTP calls), making it unsuitable for client-side usage
- No push updates: Flagr doesn't provide webhooks for flag changes, so
PROVIDER_CONFIGURATION_CHANGEDevents are not emitted - Edge Runtime: Not compatible with Edge Runtime (requires Node.js HTTP)
- No Batch Evaluation: OpenFeature doesn't support batch evaluation. This is something I want to add within the provider initialiser