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
| GrowthBook | A 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 |
FeatureResult | Flag<T> |
FeatureResult.value | Flag<T>.value |
FeatureResult.on | Flag<T>.isEnabled() |
FeatureResult.source (string) | Flag<T>.source (typed EvaluationSource) |
FeatureResult.experiment | Flag<T>.ruleId + .ruleType |
FeatureResult.experimentResult.variationId | Flag<T>.variationKey |
GrowthBook constructor attributes | EvalContext 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:
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();A vs B, Node:
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 });});GrowthBook, 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>A vs B, React:
import { AvsbProvider } from '@avsbhq/react';<AvsbProvider sdkKey="sdk_production_xxxxxxxxxxxxxxxx" context={{ kind: 'user', key: 'u_123', plan: 'pro', country: 'US' }}> <YourApp /></AvsbProvider>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:
gb.setAttributes({ id: 'u_456', plan: 'enterprise', country: 'UK' });A vs B:
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' });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:
// 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.experimentResultA vs B:
// 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[]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:
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 analyticsA vs B:
// 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 });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):
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);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:
// Manual refreshawait gb.refreshFeatures();// Or a real-time streamawait gb.init({ streaming: true });A vs B:
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'});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:
// 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 });A vs B, SSR bootstrap:
import { fetchDatafile } from '@avsbhq/browser/server';const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}> <YourApp /></AvsbProvider>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:
import { LocalStorageStickyBucketService } from '@growthbook/growthbook';const gb = new GrowthBook({ stickyBucketService: new LocalStorageStickyBucketService(),});A vs B, sticky service:
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 }),});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:
const flag = server.forUser(ctx).getBoolFlag('checkout_redesign', false);if (flag.source === 'holdout') { // This user is in the holdout cohort: exclude from experiment analysis}Cleanup
GrowthBook:
gb.destroy();A vs B:
await client.close(); // flushes events, stops pollingTesting
GrowthBook, test instance:
const gb = new GrowthBook({ features: { show_banner: { defaultValue: true }, homepage_theme: { defaultValue: 'blue' }, },});A vs B, mock client:
import { createMockClient } from '@avsbhq/test';const mock = createMockClient({ flags: { show_banner: true, homepage_theme: 'blue', },});Cutover checklist
Remove GrowthBook packages
Uninstall @growthbook/growthbook and @growthbook/growthbook-react from your project.
Install A vs B packages
Install @avsbhq/node, @avsbhq/browser, and @avsbhq/react as needed.
Swap SDK key
Replace your GrowthBook client key with your A vs B SDK key from the Environments page.
Migrate attributes to EvalContext
Wrap the GrowthBook attributes object in an EvalContext: add kind: 'user' and rename id to key.
Replace isOn / isOff / getFeatureValue
Replace gb.isOn(key) with client.getBoolFlag(key, false).isEnabled(). Replace gb.getFeatureValue(key, default) with the appropriate typed evaluator.
Migrate trackingCallback to event listener
Move your trackingCallback logic into a client.on('exposure', ...) listener. Add explicit client.track() calls for conversion events.
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.
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.
Update tests
Replace inline features objects with createMockClient from @avsbhq/test.
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.