From GrowthBook

GrowthBook and A vs B work alike under the hood. Both evaluate flags locally against a downloaded datafile: a JSON file listing every live flag and its rules, refreshed on a timer. Neither sends evaluation data to a server just to read a flag.

A vs B ships a managed platform for building experiments. It returns one typed result from every read, instead of separate isOn / getFeatureValue calls. And it replaces GrowthBook's trackingCallback pattern with plain event listeners. Your feature keys and experiment variation keys transfer directly.

Concept mapping

GrowthBookA vs B
Feature (evalFeature)Flag (getFlag / typed evaluators)
gb.isOn(key)client.getBoolFlag(key, false).isEnabled()
gb.isOff(key)!client.getBoolFlag(key, false).isEnabled()
gb.getFeatureValue(key, default)client.getFlag(key, default).value
FeatureResultFlag<T>
FeatureResult.valueFlag<T>.value
FeatureResult.onFlag<T>.isEnabled()
FeatureResult.source (string)Flag<T>.source (typed EvaluationSource)
FeatureResult.experimentFlag<T>.ruleId + .ruleType
FeatureResult.experimentResult.variationIdFlag<T>.variationKey
GrowthBook constructor attributesEvalContext passed at construction or via identify
gb.setAttributes(attrs)client.updateAttributes(partial)
trackingCallback(experiment, result)client.on('exposure', handler) + explicit track()
Inline experiment (gb.run(experiment))Flag with an A/B test rule configured in the dashboard
gb.destroy()client.close()

Client construction

GrowthBook is constructed with an attributes object and optional features/datafile. A vs B uses an EvalContext (the same concept) and fetches its own datafile from the CDN.

GrowthBook, Node:

TypeScript
import { GrowthBook } from '@growthbook/growthbook';const gb = new GrowthBook({  apiHost: 'https://cdn.growthbook.io',  clientKey: 'sdk-abc123',  attributes: {    id: 'u_123',    plan: 'pro',    country: 'US',  },  trackingCallback: (experiment: { key: string }, result: { variationId: number }) => {    analytics.track('Experiment Viewed', { experiment_id: experiment.key, variation_id: result.variationId });  },});await gb.loadFeatures();
TypeScript15 lines

A vs B, Node:

TypeScript
import { AvsbServer } from '@avsbhq/node';const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! });const result = await server.onReady();// Wire exposure events to your analytics sinkserver.on('exposure', (event) => {  analytics.track('Experiment Viewed', { experiment_id: event.experimentId, variation_id: event.variationId, flag_key: event.properties?.flagKey });});
TypeScript9 lines

GrowthBook, React:

TypeScript React
import { GrowthBookProvider, useFeatureIsOn } from '@growthbook/growthbook-react';import { GrowthBook } from '@growthbook/growthbook';const gb = new GrowthBook({ clientKey: 'sdk-abc123', /* ... */ });await gb.loadFeatures();<GrowthBookProvider growthbook={gb}>  <YourApp /></GrowthBookProvider>
TypeScript React9 lines

A vs B, React:

TypeScript React
import { AvsbProvider } from '@avsbhq/react';<AvsbProvider  sdkKey="sdk_production_xxxxxxxxxxxxxxxx"  context={{ kind: 'user', key: 'u_123', plan: 'pro', country: 'US' }}>  <YourApp /></AvsbProvider>
TypeScript React8 lines

Identity model

GrowthBook stores attributes on the instance and updates them via setAttributes. A vs B uses the same stateful model on the browser client: identify replaces the full context and updateAttributes patches specific fields.

GrowthBook:

TypeScript
gb.setAttributes({ id: 'u_456', plan: 'enterprise', country: 'UK' });
TypeScript1 line

A vs B:

TypeScript
import { AvsbClient } from '@avsbhq/browser';const client = new AvsbClient({  sdkKey: 'sdk_production_xxxxxxxxxxxxxxxx',  context: { kind: 'user', key: 'u_123', plan: 'pro', country: 'US' },});// Replace the full context (equivalent to setAttributes with a new full object)client.identify({ kind: 'user', key: 'u_456', plan: 'enterprise', country: 'UK' });// Patch only changed attributes: no need to read and spread the current contextclient.updateAttributes({ plan: 'enterprise' });// Stitch anonymous to identified identity at sign-up. Synchronous: it records// the moment, it does not move past decisions.client.identify({ kind: 'user', key: 'u_456' });client.alias(  { kind: 'user', key: 'anon_device_xyz' },  { kind: 'user', key: 'u_456' });
TypeScript20 lines
Tip

A vs B's updateAttributes explicitly patches attributes rather than silently merging. This makes it clear in your code when you intend a partial update vs a full identity replacement.

Flag evaluation

GrowthBook provides evalFeature, isOn, isOff, and getFeatureValue as separate calls. A vs B unifies all four into typed evaluators that always return a Flag<T> with convenience methods.

GrowthBook:

TypeScript
// Boolean checkconst showBanner = gb.isOn('show_banner');const hideBanner = gb.isOff('show_banner');// Typed valueconst theme = gb.getFeatureValue('homepage_theme', 'default');// Full result with metadataconst result = gb.evalFeature<string>('homepage_theme');// result.value, result.on, result.source, result.experiment, result.experimentResult
TypeScript10 lines

A vs B:

TypeScript
// Boolean: isEnabled() checks both source and valueconst showBanner = client.getBoolFlag('show_banner', false).isEnabled();const hideBanner = !client.getBoolFlag('show_banner', false).isEnabled();// Typed value shorthandconst theme = client.getStringFlag('homepage_theme', 'default').value;// Full envelope: equivalent to evalFeatureconst flag = client.getStringFlag('homepage_theme', 'default');flag.value          // 'blue' | 'green' | 'default'flag.isEnabled()    // true only for a real decision (rule, holdout, bandit, or override) with a truthy valueflag.variationKey   // 'treatment_a' | 'control' | nullflag.source         // 'rule' | 'default' | 'holdout' | 'sticky' | ...flag.ruleId         // matched rule id or nullflag.ruleType       // 'ab_test' | 'targeted_delivery' | 'holdout' | nullflag.reasons        // string[]
TypeScript16 lines

Tracking events

GrowthBook routes exposure events through a trackingCallback defined at construction. A vs B separates exposure events (automatic, emitted by the SDK) from conversion events (explicit track calls). Wire exposures to your analytics sink via the exposure event; send conversions with track.

GrowthBook:

TypeScript
const gb = new GrowthBook({  trackingCallback: (experiment: { key: string }, result: { variationId: number }) => {    analytics.track('Experiment Viewed', { experimentId: experiment.key, variationId: result.variationId });  },});// No built-in conversion event method: use your own analytics
TypeScript7 lines

A vs B:

TypeScript
// Wire exposure events to your analytics sinkclient.on('exposure', ({ experimentId, variationId, properties }) => {  analytics.track('Experiment Viewed', { experimentId, variationId, flagKey: properties?.flagKey });});// Send conversion events explicitly. `revenue` is money in major units;// `value` is the separate numeric-metric column.client.track('purchase_completed', { revenue: 49.99 });
TypeScript8 lines

Multi-context

GrowthBook targets on a single flat attributes object. A vs B adds multi-context targeting, a new capability on migration. You can bucket and target simultaneously on user, organization, device, and any other entity kind you define in the dashboard.

A vs B, multi-context (new capability):

TypeScript
import type { MultiContext } from '@avsbhq/core';const ctx: MultiContext = {  kind: 'multi',  user:         { kind: 'user',         key: 'u_123', plan: 'pro' },  organization: { kind: 'organization', key: 'org_42', tier: 'enterprise' },};server.forUser(ctx).getBoolFlag('enterprise_feature', false);
TypeScript8 lines

Streaming updates

GrowthBook refreshes its feature payload on demand, or in real time over a stream. A vs B polls on a timer with optional SSE push for the browser client.

GrowthBook:

TypeScript
// Manual refreshawait gb.refreshFeatures();// Or a real-time streamawait gb.init({ streaming: true });
TypeScript5 lines

A vs B:

TypeScript
const client = new AvsbClient({  sdkKey: 'sdk_production_xxxxxxxxxxxxxxxx',  context: { kind: 'user', key: 'u_123' },  pollingInterval: 30_000, // ms});client.on('configUpdate', ({ reason }) => {  // 'poll' | 'stream' | 'manual'});
TypeScript9 lines

Bootstrap / SSR

GrowthBook supports passing an inline features map at construction to skip the initial network request. A vs B uses a pre-fetched FlagDatafile passed as bootstrap.

GrowthBook, SSR:

TypeScript
// Fetch features server-sideconst res = await fetch('https://cdn.growthbook.io/api/features/sdk-abc123');const json = await res.json();const gb = new GrowthBook({ features: json.features });
TypeScript5 lines

A vs B, SSR bootstrap:

TypeScript React
import { fetchDatafile } from '@avsbhq/browser/server';const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}>  <YourApp /></AvsbProvider>
TypeScript React6 lines

Sticky bucketing

GrowthBook supports sticky bucketing via a StickyBucketService interface. A vs B uses the same interface name and the same two methods, with a richer StickyAssignment payload that includes rule context and an assignment timestamp. One difference to plan for: A vs B configures sticky bucketing on the server SDK (@avsbhq/node). A browser-only GrowthBook setup using LocalStorageStickyBucketService moves server-side on migration. @avsbhq/node ships InMemoryStickyBucketService and RedisStickyBucketService, or you can implement the interface over your own store.

GrowthBook, sticky service:

TypeScript
import { LocalStorageStickyBucketService } from '@growthbook/growthbook';const gb = new GrowthBook({  stickyBucketService: new LocalStorageStickyBucketService(),});
TypeScript4 lines

A vs B, sticky service:

TypeScript
import { AvsbServer, RedisStickyBucketService } from '@avsbhq/node';import { createClient } from 'redis';const redis = createClient({ url: process.env.REDIS_URL });await redis.connect();const server = new AvsbServer({  sdkKey: process.env.AVSB_SDK_KEY!,  stickyBucketService: new RedisStickyBucketService({ redis }),});
TypeScript10 lines

Rather not add Redis? Implement the same two-method StickyBucketService interface (lookup, save) over your own store instead, the same shape GrowthBook asks for.

Holdouts

GrowthBook has a holdout concept available on its Enterprise plan. A vs B includes holdouts on all plans. Configure a holdout in the dashboard and associate flags with it. Held-out visitors automatically receive source: 'holdout' in the evaluation result.

A vs B, detecting holdout traffic:

TypeScript
const flag = server.forUser(ctx).getBoolFlag('checkout_redesign', false);if (flag.source === 'holdout') {  // This user is in the holdout cohort: exclude from experiment analysis}
TypeScript4 lines

Cleanup

GrowthBook:

TypeScript
gb.destroy();
TypeScript1 line

A vs B:

TypeScript
await client.close(); // flushes events, stops polling
TypeScript1 line

Testing

GrowthBook, test instance:

TypeScript
const gb = new GrowthBook({  features: {    show_banner: { defaultValue: true },    homepage_theme: { defaultValue: 'blue' },  },});
TypeScript6 lines

A vs B, mock client:

TypeScript
import { createMockClient } from '@avsbhq/test';const mock = createMockClient({  flags: {    show_banner: true,    homepage_theme: 'blue',  },});
TypeScript8 lines

Cutover checklist

1

Remove GrowthBook packages

Uninstall @growthbook/growthbook and @growthbook/growthbook-react from your project.

2

Install A vs B packages

Install @avsbhq/node, @avsbhq/browser, and @avsbhq/react as needed.

3

Swap SDK key

Replace your GrowthBook client key with your A vs B SDK key from the Environments page.

4

Migrate attributes to EvalContext

Wrap the GrowthBook attributes object in an EvalContext: add kind: 'user' and rename id to key.

5

Replace isOn / isOff / getFeatureValue

Replace gb.isOn(key) with client.getBoolFlag(key, false).isEnabled(). Replace gb.getFeatureValue(key, default) with the appropriate typed evaluator.

6

Migrate trackingCallback to event listener

Move your trackingCallback logic into a client.on('exposure', ...) listener. Add explicit client.track() calls for conversion events.

7

Migrate inline experiments

GrowthBook inline experiments (gb.run()) have no direct equivalent. Create the experiment as a flag with an A/B test rule in the dashboard, then evaluate it with getFlag.

8

Migrate sticky bucket service (if used)

Reimplement your StickyBucketService on @avsbhq/node. lookup and save now take StickyAssignment objects, and you pass the service to AvsbServer, not to the browser client.

9

Update tests

Replace inline features objects with createMockClient from @avsbhq/test.

10

Verify and deploy

Run npm run build and npx tsc --noEmit. Confirm flags and exposure events appear in the A vs B dashboard before promoting to production.

Was this helpful?