/Docs

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

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
ts
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();
A vs B — Node
ts
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 sink
7server.on('exposure', (event) => {
8 analytics.track('Experiment Viewed', {
9 experiment_id: event.flagKey,
10 variation_id: event.variationKey,
11 });
12});
GrowthBook — React
ts
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>
A vs B — React
ts
1import { AvsbProvider } from '@avsbhq/react';
2
3<AvsbProvider
4 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.

GrowthBook
ts
1// Full attribute replacement
2gb.setAttributes({ id: 'u_456', plan: 'enterprise', country: 'UK' });
3
4// Merge with existing — GrowthBook silently merges in some versions
5gb.setAttributes({ ...gb.getAttributes(), plan: 'enterprise' });
A vs B
ts
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 context
5client.updateAttributes({ plan: 'enterprise' });
6
7// Stitch anonymous → identified identity at sign-up
8await client.alias(
9 { kind: 'user', key: 'anon_device_xyz' },
10 { kind: 'user', key: 'u_456' }
11);
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
ts
1// Boolean check
2const showBanner = gb.isOn('show_banner');
3const hideBanner = gb.isOff('show_banner');
4
5// Typed value
6const theme = gb.getFeatureValue('homepage_theme', 'default');
7
8// Full result with metadata
9const result = gb.evalFeature<string>('homepage_theme');
10// result.value, result.on, result.source, result.experiment, result.experimentResult
A vs B
ts
1// Boolean — isEnabled() checks both source and value
2const showBanner = client.getBoolFlag('show_banner', false).isEnabled();
3const hideBanner = !client.getBoolFlag('show_banner', false).isEnabled();
4
5// Typed value shorthand
6const theme = client.getStringFlag('homepage_theme', 'default').value;
7
8// Full envelope — equivalent to evalFeature
9const flag = client.getStringFlag('homepage_theme', 'default');
10flag.value // 'blue' | 'green' | 'default'
11flag.isEnabled() // true if a rule matched and value is truthy
12flag.variationKey // 'treatment_a' | 'control' | null
13flag.source // 'rule' | 'default' | 'holdout' | 'sticky' | ...
14flag.ruleId // matched rule id or null
15flag.ruleType // 'ab_test' | 'targeted_delivery' | 'holdout' | null
16flag.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
ts
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 analytics
A vs B
ts
1// Wire exposure events to your analytics sink
2client.on('exposure', ({ flagKey, variationKey, contextKeys }) => {
3 segment.track('Experiment Viewed', {
4 experimentId: flagKey,
5 variationId: variationKey,
6 });
7});
8
9// Send conversion events explicitly
10client.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.

A vs B — multi-context (new capability)
ts
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.

GrowthBook
ts
1gb.setRefreshRate(30); // seconds
A vs B
ts
1const client = new AvsbClient({
2 sdkKey: 'sdk_production_abc123',
3 context: { kind: 'user', key: 'u_123' },
4 pollingInterval: 30_000, // ms
5});
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.

GrowthBook — SSR
ts
1// Fetch features server-side
2const res = await fetch('https://cdn.growthbook.io/api/features/sdk-abc123');
3const json = await res.json();
4
5const gb = new GrowthBook({ features: json.features });
A vs B — SSR bootstrap
ts
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.

GrowthBook — sticky service
ts
1import { LocalStorageStickyBucketService } from '@growthbook/growthbook';
2const gb = new GrowthBook({
3 stickyBucketService: new LocalStorageStickyBucketService(),
4});
A vs B — sticky service
ts
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.

A vs B — detecting holdout traffic
ts
1const flag = server.forUser(ctx).getBoolFlag('checkout_redesign', false);
2if (flag.source === 'holdout') {
3 // This user is in the holdout cohort — exclude from experiment analysis
4}

Cleanup

GrowthBook
ts
1gb.destroy();
A vs B
ts
1await client.close(); // flushes events, stops polling

Testing

GrowthBook — test instance
ts
1const gb = new GrowthBook({
2 features: {
3 show_banner: { defaultValue: true },
4 homepage_theme: { defaultValue: 'blue' },
5 },
6});
A vs B — mock client
ts
1import { createMockClient } from '@avsbhq/test';
2
3const mock = createMockClient({
4 flags: {
5 show_banner: true,
6 homepage_theme: 'blue',
7 },
8});

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 A vs B dashboard and evaluate it via getFlag.
8

Migrate sticky bucket service (if used)

Reimplement your StickyBucketService with the A vs B interface — lookup and save now use StickyAssignment objects.
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.