OpenFeature (JavaScript)

OpenFeature is a vendor-neutral API for reading feature flags. Your application code calls client.getBooleanValue(...), and a provider underneath decides where the answer comes from.

This page is the honest state of that integration for JavaScript and TypeScript: what A vs B ships today, the small amount of glue the OpenFeature server SDK needs on top of it, and the parts of A vs B that the OpenFeature API cannot express.

If you are choosing between the two, the native @avsbhq/browser and @avsbhq/node SDKs give you the full surface (multi-context identity, bandits, sticky bucketing, decision logs, evaluation reasons). Use OpenFeature when your codebase is already written against it.


What ships today

A vs B publishes two adapter classes on the @avsbhq/utils/openfeature entry point:

TypeScript
// Both classes, and every type named below, come from// '@avsbhq/utils/openfeature'.import type {  BrowserEvaluator,  OpenFeatureEvaluationContext,  ResolutionDetails,  ServerEvaluator,} from '@avsbhq/utils/openfeature'import type { TrackPayload } from '@avsbhq/core'// The server one, wrapping AvsbServer from @avsbhq/nodeclass AvsbProvider {  metadata: { name: 'avsb' }  constructor(server: ServerEvaluator)  resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, ctx: OpenFeatureEvaluationContext): ResolutionDetails<boolean>  resolveStringEvaluation(flagKey: string, defaultValue: string, ctx: OpenFeatureEvaluationContext): ResolutionDetails<string>  resolveNumberEvaluation(flagKey: string, defaultValue: number, ctx: OpenFeatureEvaluationContext): ResolutionDetails<number>  resolveObjectEvaluation<T>(flagKey: string, defaultValue: T, ctx: OpenFeatureEvaluationContext): ResolutionDetails<T>  track(eventKey: string, ctx: OpenFeatureEvaluationContext, payload?: TrackPayload): void}// The browser one, wrapping AvsbClient from @avsbhq/browserclass AvsbWebProvider {  metadata: { name: 'avsb-web' }  constructor(client: BrowserEvaluator)  resolveBooleanEvaluation(flagKey: string, defaultValue: boolean): ResolutionDetails<boolean>  resolveStringEvaluation(flagKey: string, defaultValue: string): ResolutionDetails<string>  resolveNumberEvaluation(flagKey: string, defaultValue: number): ResolutionDetails<number>  resolveObjectEvaluation<T>(flagKey: string, defaultValue: T): ResolutionDetails<T>}
TypeScript30 lines

Two things about them are worth knowing before you wire either one up:

  1. They do not import the OpenFeature packages. They are written against the shapes OpenFeature defines, so installing @avsbhq/utils does not pull an OpenFeature SDK into your build. In practice that means TypeScript is comparing two independent declarations of the same shape, and it can reject the handoff. The wrapper in the next section is the fix, and it is short.
  2. Resolution is synchronous in both. That matches the OpenFeature web SDK, which resolves synchronously. The OpenFeature server SDK expects providers to resolve asynchronously, which is what the wrapper below is mostly doing.
Info

There is no published @avsbhq/openfeature package, no hosted OFREP endpoint, and no OpenFeature conformance suite in our CI. What is on this page is what exists.


Server (@openfeature/server-sdk)

Wrap the shipped adapter in a provider typed against the OpenFeature package you installed. This is the whole thing, and it compiles under strict mode:

TypeScript
// avsb-provider.ts// docs-example: not typechecked here, because this repo does not install// @openfeature/server-sdk. Copy it into an app that does.import {  type EvaluationContext,  type JsonValue,  type Provider,  type ResolutionDetails,} from '@openfeature/server-sdk'import { AvsbProvider as AvsbResolver } from '@avsbhq/utils/openfeature'import type { AvsbServer } from '@avsbhq/node'export class AvsbOpenFeatureProvider implements Provider {  readonly metadata = { name: 'avsb' } as const  readonly runsOn = 'server' as const  private readonly resolver: AvsbResolver  constructor(private readonly server: AvsbServer) {    this.resolver = new AvsbResolver(server)  }  async initialize(): Promise<void> {    await this.server.onReady()  }  async onClose(): Promise<void> {    await this.server.close()  }  async resolveBooleanEvaluation(    flagKey: string,    defaultValue: boolean,    context: EvaluationContext,  ): Promise<ResolutionDetails<boolean>> {    const { value, variant, reason } = this.resolver.resolveBooleanEvaluation(      flagKey,      defaultValue,      context,    )    return { value, variant, reason }  }  async resolveStringEvaluation(    flagKey: string,    defaultValue: string,    context: EvaluationContext,  ): Promise<ResolutionDetails<string>> {    const { value, variant, reason } = this.resolver.resolveStringEvaluation(      flagKey,      defaultValue,      context,    )    return { value, variant, reason }  }  async resolveNumberEvaluation(    flagKey: string,    defaultValue: number,    context: EvaluationContext,  ): Promise<ResolutionDetails<number>> {    const { value, variant, reason } = this.resolver.resolveNumberEvaluation(      flagKey,      defaultValue,      context,    )    return { value, variant, reason }  }  async resolveObjectEvaluation<T extends JsonValue>(    flagKey: string,    defaultValue: T,    context: EvaluationContext,  ): Promise<ResolutionDetails<T>> {    const { value, variant, reason } = this.resolver.resolveObjectEvaluation<T>(      flagKey,      defaultValue,      context,    )    return { value, variant, reason }  }}
TypeScript82 lines

Then register it once, at startup:

TypeScript
// docs-example: not typechecked here, because this repo does not install// @openfeature/server-sdk.import { OpenFeature } from '@openfeature/server-sdk'import { AvsbServer } from '@avsbhq/node'import { AvsbOpenFeatureProvider } from './avsb-provider'const sdkKey = process.env.AVSB_SDK_KEYif (!sdkKey) throw new Error('AVSB_SDK_KEY is not set')const server = new AvsbServer({ sdkKey })await OpenFeature.setProviderAndWait(new AvsbOpenFeatureProvider(server))const client = OpenFeature.getClient()const details = await client.getBooleanDetails('new_checkout_flow', false, {  targetingKey: 'u_123',  plan: 'pro',})// details.value, details.variant, details.reason
TypeScript19 lines

setProviderAndWait matters: it awaits initialize(), which awaits the first datafile. Registering without waiting means the first few evaluations run before any flag configuration exists, and every one of them returns the default you passed.


Browser (@openfeature/web-sdk)

The web SDK resolves synchronously, which is what the shipped adapter already does:

TypeScript
// docs-example: not typechecked here, because this repo does not install// @openfeature/web-sdk.import { OpenFeature } from '@openfeature/web-sdk'import { AvsbWebProvider } from '@avsbhq/utils/openfeature'import { AvsbClient } from '@avsbhq/browser'const avsb = new AvsbClient({ sdkKey: import.meta.env.VITE_AVSB_SDK_KEY })await avsb.onReady()await OpenFeature.setProviderAndWait(new AvsbWebProvider(avsb))const client = OpenFeature.getClient()const showBanner = client.getBooleanValue('show-promo-banner', false)
TypeScript13 lines
Your SDK key is public
Your SDK key is a public identifier, not a secret: it is safe to ship in browser and mobile bundles, it can only fetch that environment's flag configuration and send events, and it can never read or change anything in your dashboard. Credentials covers all four A vs B credentials and which one to reach for.

If TypeScript rejects new AvsbWebProvider(avsb) where a Provider is expected, wrap it the same way as the server example: a class that implements Provider, holding an AvsbWebProvider and returning { value, variant, reason } from each resolver.

Identity in the browser

Either path works, and they do the same thing.

OpenFeature.setContext(...) reaches the A vs B client: the web adapter's onContextChange calls identify on it, so every read after that evaluates, buckets and attributes against the new subject. The whole context travels, not just the targeting key.

TypeScript
await OpenFeature.setContext({ targetingKey: 'u_123', plan: 'pro' })// equivalent to:avsb.identify({ kind: 'user', key: 'u_123', plan: 'pro' })
TypeScript3 lines

Use identify directly when you want a kind other than user, which OpenFeature's single-context model cannot express.

Worth knowing if you are upgrading: this used to be a silent no-op. The adapter forwarded context changes to a setContext method the browser SDK does not have, and because the call was optional it compiled and did nothing, so flags kept evaluating for the previous visitor. If you added an identify call as a workaround, it is still correct and still the same operation; you can keep it or drop it.


How the two models map

OpenFeatureA vs BNotes
targetingKeycontext.keyThe bucketing identifier.
Any other context propertyA targeting attributeAttached to a user kind context.
Multi-kind contextNot expressibleThe adapter always builds a single user context. Use the native SDK for { kind: 'multi', … }.
ResolutionDetails.valueFlag.value
ResolutionDetails.variantFlag.variationKeyundefined when the default was served.
ResolutionDetails.reasonFlag.source, upper-casedRULE, HOLDOUT, BANDIT, STICKY, DATAFILEOVERRIDE, RUNTIMEOVERRIDE, DEFAULT, DISABLED. These are the A vs B sources upper-cased whole (no separator added), not OpenFeature's standard reason strings. The two error sources are the exception, see the row below.
flagMetadataruleId, ruleType, durationMicros
errorCodeSet for the two error sourcesA flag that is missing, or whose declared type contradicts the getter you called, resolves as reason: 'ERROR' with errorCode: 'FLAG_NOT_FOUND'. Reading before the SDK is ready gives errorCode: 'PROVIDER_NOT_READY'. Your default value is still returned in both cases, and errorMessage says which of the two it was.
Provider hooks, events, statusNot implemented

The typed getters are honoured

getBooleanValue, getStringValue, getNumberValue and getObjectValue each route through the matching A vs B typed getter, so the type the platform declares for a flag is checked at read time. Call getBooleanValue on a flag configured as JSON and you get your default back with reason: 'ERROR', not the object mislabelled as a boolean.

One rough edge to know about: a type mismatch and a genuinely missing flag both report errorCode: 'FLAG_NOT_FOUND'. The SDK returns the same "no usable value" result for both, and only errorMessage distinguishes them. Read that string when you need to tell the two apart.

Evaluations record exposures

Every resolution goes through the normal A vs B evaluation path, so it records an exposure for experiment rules exactly as a native read does. That is usually what you want on the server, where a read happens once per request for one visitor.

In the browser it deserves a second thought: OpenFeature reads are cheap and code tends to call them during rendering, and every one of those is an exposure. If you read a flag in a component that re-renders, prefer the native @avsbhq/browser client with { readOnly: true }, or one of the framework SDKs, whose render-path reads never fire exposures.

Tracking events

The server adapter forwards OpenFeature's tracking call:

TypeScript
// docs-example: not typechecked here, because this repo does not install// @openfeature/server-sdk.resolver.track('checkout_started', { targetingKey: 'u_123' }, { value: 99 })
TypeScript3 lines

The web adapter has no tracking method. Call the A vs B client directly:

TypeScript
avsb.track('checkout_started', { value: 99 })
TypeScript1 line

When to use the native SDK instead

  • Multi-context targeting: one evaluation against a user and an organization at once.
  • Bandits and holdouts you want to reason about, using flag.source and flag.reasons.
  • Sticky bucketing with your own storage.
  • Decision logs for auditing what a request decided.
  • Exposure control: render-safe reads, and manualExposure where the visitor actually sees the variation.

Everything above is available through the OpenFeature adapters only as far as ResolutionDetails can carry it, which is the value, the variant, and a reason string.


Was this helpful?