Migrate from GrowthBook to A vs B
GrowthBook and A vs B share a similar open-source philosophy — both evaluate flags locally against a downloaded feature definition file and neither sends evaluation data server-side by default. The main differences are that A vs B ships a managed platform for experiment authoring, uses a formally typed evaluation envelope instead of separate isOn / getFeatureValuecalls, and replaces GrowthBook's trackingCallback pattern with an explicit event-bus model. 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.
1import { GrowthBook } from '@growthbook/growthbook';2
3const gb = new GrowthBook({4 apiHost: 'https://cdn.growthbook.io',5 clientKey: 'sdk-abc123',6 attributes: {7 id: 'u_123',8 plan: 'pro',9 country: 'US',10 },11 trackingCallback: (experiment, result) => {12 analytics.track('Experiment Viewed', {13 experiment_id: experiment.key,14 variation_id: result.variationId,15 });16 },17});18await gb.loadFeatures();1import { AvsbServer } from '@avsbhq/node';2
3const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! });4const result = await server.onReady();5
6// Wire exposure events to your analytics sink7server.on('exposure', (event) => {8 analytics.track('Experiment Viewed', {9 experiment_id: event.flagKey,10 variation_id: event.variationKey,11 });12});1import { GrowthBookProvider, useFeatureIsOn } from '@growthbook/growthbook-react';2import { GrowthBook } from '@growthbook/growthbook';3
4const gb = new GrowthBook({ clientKey: 'sdk-abc123', /* ... */ });5await gb.loadFeatures();6
7<GrowthBookProvider growthbook={gb}>8 <App />9</GrowthBookProvider>1import { AvsbProvider } from '@avsbhq/react';2
3<AvsbProvider4 sdkKey="sdk_production_abc123"5 context={{ kind: 'user', key: 'u_123', plan: 'pro', country: 'US' }}6>7 <App />8</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.
1// Full attribute replacement2gb.setAttributes({ id: 'u_456', plan: 'enterprise', country: 'UK' });3
4// Merge with existing — GrowthBook silently merges in some versions5gb.setAttributes({ ...gb.getAttributes(), plan: 'enterprise' });1// Replace the full context (equivalent to setAttributes with a new full object)2client.identify({ kind: 'user', key: 'u_456', plan: 'enterprise', country: 'UK' });3
4// Patch only changed attributes — no need to read and spread the current context5client.updateAttributes({ plan: 'enterprise' });6
7// Stitch anonymous → identified identity at sign-up8await client.alias(9 { kind: 'user', key: 'anon_device_xyz' },10 { kind: 'user', key: 'u_456' }11);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.
1// Boolean check2const showBanner = gb.isOn('show_banner');3const hideBanner = gb.isOff('show_banner');4
5// Typed value6const theme = gb.getFeatureValue('homepage_theme', 'default');7
8// Full result with metadata9const result = gb.evalFeature<string>('homepage_theme');10// result.value, result.on, result.source, result.experiment, result.experimentResult1// Boolean — isEnabled() checks both source and value2const showBanner = client.getBoolFlag('show_banner', false).isEnabled();3const hideBanner = !client.getBoolFlag('show_banner', false).isEnabled();4
5// Typed value shorthand6const theme = client.getStringFlag('homepage_theme', 'default').value;7
8// Full envelope — equivalent to evalFeature9const flag = client.getStringFlag('homepage_theme', 'default');10flag.value // 'blue' | 'green' | 'default'11flag.isEnabled() // true if a rule matched and value is truthy12flag.variationKey // 'treatment_a' | 'control' | null13flag.source // 'rule' | 'default' | 'holdout' | 'sticky' | ...14flag.ruleId // matched rule id or null15flag.ruleType // 'ab_test' | 'targeted_delivery' | 'holdout' | null16flag.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.
1const gb = new GrowthBook({2 trackingCallback: (experiment, result) => {3 segment.track('Experiment Viewed', {4 experimentId: experiment.key,5 variationId: result.variationId,6 });7 },8});9
10// No built-in conversion event method — use your own analytics1// Wire exposure events to your analytics sink2client.on('exposure', ({ flagKey, variationKey, contextKeys }) => {3 segment.track('Experiment Viewed', {4 experimentId: flagKey,5 variationId: variationKey,6 });7});8
9// Send conversion events explicitly10client.track('purchase_completed', {11 value: 49.99,12 properties: { orderId: 'ord_99' },13});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.
1import type { MultiContext } from '@avsbhq/core';2
3const ctx: MultiContext = {4 kind: 'multi',5 user: { kind: 'user', key: 'u_123', plan: 'pro' },6 organization: { kind: 'organization', key: 'org_42', tier: 'enterprise' },7};8server.forUser(ctx).getBoolFlag('enterprise_feature', false);Streaming updates
GrowthBook uses a configurable refresh interval. A vs B uses the same polling model with optional SSE push for the browser client.
1gb.setRefreshRate(30); // seconds1const client = new AvsbClient({2 sdkKey: 'sdk_production_abc123',3 context: { kind: 'user', key: 'u_123' },4 pollingInterval: 30_000, // ms5});6
7client.on('configUpdate', ({ reason }) => {8 // 'poll' | 'stream' | 'manual'9});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.
1// Fetch features server-side2const res = await fetch('https://cdn.growthbook.io/api/features/sdk-abc123');3const json = await res.json();4
5const gb = new GrowthBook({ features: json.features });1import { fetchDatafile } from '@avsbhq/node/server';2const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);3
4<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}>5 {children}6</AvsbProvider>Sticky bucketing
GrowthBook supports sticky bucketing via a StickyBucketService interface. A vs B uses the same interface name and concept but with a richer StickyAssignment payload that includes rule context and an assignment timestamp.
1import { LocalStorageStickyBucketService } from '@growthbook/growthbook';2const gb = new GrowthBook({3 stickyBucketService: new LocalStorageStickyBucketService(),4});1import { StickyBucketService, StickyAssignment } from '@avsbhq/core';2
3class LocalStorageSticky implements StickyBucketService {4 lookup(userId: string, flagKey: string): StickyAssignment | null {5 const raw = localStorage.getItem(`sticky_${userId}_${flagKey}`);6 return raw ? (JSON.parse(raw) as StickyAssignment) : null;7 }8 save(userId: string, flagKey: string, assignment: StickyAssignment): void {9 localStorage.setItem(`sticky_${userId}_${flagKey}`, JSON.stringify(assignment));10 }11}12
13const client = new AvsbClient({14 sdkKey: 'sdk_production_abc123',15 context: { kind: 'user', key: 'u_123' },16 stickyBucketService: new LocalStorageSticky(),17});Holdouts
GrowthBook has a holdout concept available on its Pro plan. A vs B includes holdouts on all plans. Configure a holdout in the dashboard and associate flags with it — held-out users automatically receive source: 'holdout' in the evaluation result.
1const flag = server.forUser(ctx).getBoolFlag('checkout_redesign', false);2if (flag.source === 'holdout') {3 // This user is in the holdout cohort — exclude from experiment analysis4}Cleanup
1gb.destroy();1await client.close(); // flushes events, stops pollingTesting
1const gb = new GrowthBook({2 features: {3 show_banner: { defaultValue: true },4 homepage_theme: { defaultValue: 'blue' },5 },6});1import { createMockClient } from '@avsbhq/test';2
3const mock = createMockClient({4 flags: {5 show_banner: true,6 homepage_theme: 'blue',7 },8});Cutover checklist
Remove GrowthBook packages
@growthbook/growthbook and @growthbook/growthbook-react from your project.Install A vs B packages
@avsbhq/node, @avsbhq/browser, and @avsbhq/react as needed.Swap SDK key
Migrate attributes to EvalContext
EvalContext: add kind: 'user' and rename id to key.Replace isOn / isOff / getFeatureValue
gb.isOn(key) with client.getBoolFlag(key, false).isEnabled(). Replace gb.getFeatureValue(key, default) with the appropriate typed evaluator.Migrate trackingCallback to event listener
trackingCallback logic into a client.on('exposure', ...) listener. Add explicit client.track() calls for conversion events.Migrate inline experiments
gb.run()) have no direct equivalent — create the experiment as a flag with an A/B test rule in the A vs B dashboard and evaluate it via getFlag.Migrate sticky bucket service (if used)
StickyBucketService with the A vs B interface — lookup and save now use StickyAssignment objects.Update tests
features objects with createMockClient from @avsbhq/test.Verify and deploy
npm run build and npx tsc --noEmit. Confirm flags and exposure events appear in the A vs B dashboard before promoting to production.