SDK Installation

Feature flag projects use npm SDK packages rather than the web snippet. Pick the package that matches your app: each one wraps the same evaluator, so the concepts on this page are the same everywhere.

Your appInstallGuide
Vanilla JS or TypeScript in the browser@avsbhq/browserThis page
React@avsbhq/reactVite + React
Next.js@avsbhq/nextApp Router, Pages Router
Vue@avsbhq/vueVite + Vue
Nuxt@avsbhq/nuxtNuxt
Svelte@avsbhq/svelteVite + Svelte, SvelteKit
Solid@avsbhq/solidVite + Solid
Angular@avsbhq/angularAngular CLI
React Native@avsbhq/react-nativeReact Native
Node.js server@avsbhq/nodeExpress, Fastify
Edge runtimes@avsbhq/edgeEdge SDK

Each framework package depends on the client it wraps, so one install is enough: npm install @avsbhq/react brings @avsbhq/browser with it.

Get your SDK key

Open Environments in your feature flag project sidebar and copy the key for the environment you are wiring up. The format is sdk_<environment>_<id>, for example sdk_production_ttqm0eaj4vth1krcb2xn. Every environment has its own key.

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.

The client checks the shape of the key when you construct it. A key that does not look like an SDK key (a pasted dashboard URL, a truncated copy, a token) logs one actionable error naming what it got and where the real key lives, then carries on. The datafile request is the real test.

The browser SDK

Shell
npm install @avsbhq/browser
Shell1 line

Construct the client once

TypeScript
import { AvsbClient } from '@avsbhq/browser'import type { Flag, InitResult } from '@avsbhq/browser'const client = new AvsbClient({  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',  context: { kind: 'user', key: 'u_123', plan: 'pro' },})const result: InitResult = await client.onReady()if (!result.success) {  // Nothing could be loaded, so every read returns the default you pass.  // result.error names the status, the URL, and the fix.  console.warn(result.error?.message)}const checkout: Flag<boolean> = client.getBoolFlag('new_checkout_flow', false)
TypeScript16 lines

Construct it once at application boot, not per page or per component.

The constructor starts loading immediately: it validates the key, resolves a stable visitor id, serves any cached datafile, and starts the network fetch. onReady() is how you wait for the result, not how you start it. Pass autoInit: false to build the client now and load later.

Warning

context is the one identity model. The old attributes and userIdAttribute options were deleted: two models meant every SDK and every page had to teach both, and the second could not express multi-context at all. The migration is mechanical: attributes: { userId: 'u_1', plan: 'pro' } becomes context: { kind: 'user', key: 'u_1', plan: 'pro' }.

Read flags

Every read returns a Flag<T> object and takes a default value. The default is what you get before the SDK is ready, when the flag key does not exist, and when the flag's declared type contradicts the getter you called.

TypeScript
const darkMode: Flag<boolean> = client.getBoolFlag('dark_mode', false)const theme: Flag<string> = client.getStringFlag('homepage_theme', 'default')const maxItems: Flag<number> = client.getNumberFlag('max_results', 25)interface ApiConfig {  timeout: number  retries: number}const config: Flag<ApiConfig> = client.getJsonFlag<ApiConfig>('api_config', {  timeout: 5000,  retries: 3,})const timeout: number = config.value.timeout
TypeScript13 lines

The typed getters check the value against the type the platform declared for that flag. On a mismatch they never throw: one warning names the flag, the getter, and the declared type, and you get your default back with source: 'not_found'. getJsonFlag<T> checks only that the flag is declared as JSON, so validate the shape of T yourself if it crosses a trust boundary.

getFlag<T>(key, default) runs no runtime check and returns whatever the datafile says, typed as T. Prefer a typed getter when you know the type.

A flag key is lowercase letters, digits and underscores, starting with a letter, up to 100 characters: new_checkout_flow, never new-checkout-flow. The dashboard and the API refuse any other key, so a read of new-checkout-flow can never find a flag. When a read misses, the SDK logs it once for that key, and for a key the platform would refuse it names the spelling it would accept: did you mean new_checkout_flow?

Flag<T>, in full

TypeScript
import type { EvaluationSource } from '@avsbhq/browser'/** The four rule kinds a decision can come from. */type RuleType = 'targeted_delivery' | 'ab_test' | 'holdout' | 'bandit'interface FlagShape<T = unknown> {  /** The variation value, typed against the default you passed. */  readonly value: T  /** Variation key, null when the default was served or the flag is missing. */  readonly variationKey: string | null  /** Why this value was produced. */  readonly source: EvaluationSource  /** The rule that matched, null when none did. */  readonly ruleId: string | null  readonly ruleType: RuleType | null  /** Structured reasons for the decision. */  readonly reasons: string[]  /** Milliseconds since the epoch, when this was evaluated. */  readonly evaluatedAt: number  /** Microseconds spent in the evaluator. */  readonly durationMicros: number  /** A real decision produced a truthy value. */  isEnabled(): boolean  /** False for 'not_found' and for 'not_ready'. */  exists(): boolean}
TypeScript26 lines

The object is frozen, and the same object comes back on every read until that flag's value actually changes.

Every evaluation source

TypeScript
type SourceUnion =  | 'datafileOverride'  | 'runtimeOverride'  | 'sticky'  | 'rule'  | 'holdout'  | 'bandit'  | 'default'  | 'not_found'  | 'disabled'  | 'not_ready'
TypeScript11 lines
SourceMeaningisEnabled()exists()
datafileOverrideA per-user override configured in the dashboard matched.value-dependenttrue
runtimeOverridesetOverrideForUser or setGlobalOverride matched.value-dependenttrue
stickyA previously stored assignment was reused.value-dependenttrue
ruleA targeting rule or an A/B rule matched.value-dependenttrue
holdoutThe visitor is in a holdout.value-dependenttrue
banditA bandit rule picked the variation.value-dependenttrue
defaultThe flag exists, nothing matched, so its default variation was served.falsetrue
disabledThe flag exists but is switched off in this environment.falsetrue
not_foundThe datafile loaded and does not contain this key. Check the spelling.falsefalse
not_readyNo datafile yet. Ask again after onReady().falsefalse

"Value-dependent" means isEnabled() is Boolean(flag.value): a real decision was made, so the answer is whatever it produced.

Track conversions

TypeScript
client.track('purchase_completed', { revenue: 199.0 }) // moneyclient.track('items_added', { value: 3 }) // a quantityclient.track('checkout_started') // a count
TypeScript3 lines

revenue is money in decimal major units of the project currency (49.99, not 4999). value is a numeric metric value for average-value metrics (items, seats, seconds). They are two separate columns end to end, so one conversion can carry money, a quantity, or both.

Events are batched, flushed on an interval and on tab hide, retried on failure with backoff, and held (bounded) until the client is ready.

Identity

TypeScript
// Replace the whole context and re-bucket every flag.client.identify({ kind: 'user', key: 'u_123', plan: 'enterprise' })// Patch attributes on the current context, keeping the key.client.updateAttributes({ plan: 'enterprise' })// Stitch an anonymous session to the identified one, then switch.client.alias({ kind: 'user', key: 'anon_abc' }, { kind: 'user', key: 'u_123' })// Rotate to a NEW anonymous identity and clear runtime overrides.client.reset()
TypeScript11 lines

Multi-context lets one evaluation see several subjects at once. A rule can bucket on user.key while matching an audience condition on organization.tier:

TypeScript
client.identify({  kind: 'multi',  user: { kind: 'user', key: 'u_123', plan: 'pro' },  organization: { kind: 'organization', key: 'org_456', tier: 'enterprise' },})
TypeScript5 lines

Reading during a render

Framework adapters read flags while rendering, which must never write analytics. Read-only reads return the same object until the value changes and fire no exposure:

TypeScript
const unsubscribe: () => void = client.subscribe('checkout_v2', () => {  // this flag changed, or the SDK just became ready})const renderSafe: Flag<boolean> = client.getBoolFlag('checkout_v2', false, { readOnly: true })unsubscribe()
TypeScript6 lines

Fire the exposure once, where the decision is actually shown, with a normal read. In React, that is useExposure.

All flags at once

TypeScript
const all: Record<string, Flag> = client.getAllFlags()const forSomeoneElse: Record<string, Flag> = client.getAllFlags({  context: { kind: 'user', key: 'u_999', plan: 'pro' },})
TypeScript4 lines

Exposures are suppressed by default here, and the returned object is stable between changes. Before the SDK is ready it returns {} and logs one warning saying why, rather than looking like a project with no flags.

Options

TypeScript
import type { AvsbClientOptions } from '@avsbhq/browser'const options: AvsbClientOptions = {  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',  pollingInterval: 60_000, // default 60s, clamped to a 5s minimum  autoRefresh: true, // default true  cdnHost: 'https://cdn.avsb.cloud', // default  autoInit: true, // default true: start loading from the constructor  initTimeout: 10_000, // default, per datafile request  cache: true, // default true: localStorage datafile cache  cacheMaxAgeMs: 604_800_000, // default 7 days  pauseWhenHidden: true, // default true  refetchOnFocus: true, // default true  maxPendingEvents: 100, // events held before the client is ready  logLevel: 'warn', // 'silent' | 'debug' | 'info' | 'warn' | 'error'  streaming: false, // default false in the browser}
TypeScript17 lines

The datafile is cached in localStorage and served on the next visit while a refresh runs behind it, so second loads are instant and flags keep working offline. Refreshes are conditional (an unchanged datafile costs a 304 with no body), and only network responses are cached: a bootstrap datafile you pass in never is.

The default logger writes to the console at warn level in development and is silent elsewhere. Development is detected from globalThis.__AVSB_DEV__, then NODE_ENV, then the host.

Bootstrap and server rendering

Without a bootstrap, the client fetches the datafile on mount and reads return defaults until it lands. Pre-fetch it on the server and hand it over to remove that window:

TypeScript
// Serverimport { fetchDatafile } from '@avsbhq/browser/server'import type { FlagDatafile } from '@avsbhq/browser'const serverDatafile: FlagDatafile | null = await fetchDatafile(  'sdk_production_ttqm0eaj4vth1krcb2xn',  { timeout: 2000 },)// null when the fetch failed, in which case the browser fetches it itself.
TypeScript9 lines
TypeScript
// Clientimport { AvsbClient } from '@avsbhq/browser'import type { FlagDatafile } from '@avsbhq/browser'declare const datafileFromServer: FlagDatafileconst bootstrapped = new AvsbClient({  sdkKey: 'sdk_production_ttqm0eaj4vth1krcb2xn',  context: { kind: 'user', key: 'u_123' },  bootstrap: datafileFromServer,})// isReady() is already true; onReady() resolves with source: 'bootstrap'.
TypeScript12 lines

Bootstrapping guarantees that the server render and the first client render produce the same variation, which is what prevents a hydration mismatch. On Next.js, AvsbRoot from @avsbhq/next does all of this for you.

Shutting down

TypeScript
await client.flush() // drain queued eventsawait client.close() // flush, stop polling, disconnect streaming
TypeScript2 lines

close() flushes internally, so calling both is safe but redundant. track() after close() is dropped with one warning rather than silently.

Connection status

The SDK reports liveness to the platform after a successful datafile fetch, using the endpoint published in the datafile itself. Nothing needs configuring, and a failed report never affects flag delivery.

The environments page and the flag detail page show what has been heard:

  • Connected: a report arrived within the last 5 minutes.
  • Stale: the last report is older than 5 minutes.
  • Not connected: nothing has ever reported in.

A long-lived browser tab that stops polling (a hidden tab, or autoRefresh: false) will drift to Stale without anything being wrong.

What's next

Was this helpful?