/Docs

Migrate from ConfigCat to A vs B

ConfigCat is a straightforward feature flag service focused on typed value retrieval and simple targeting rules. A vs B is a superset: it covers the same flag-value retrieval surface and adds A/B test experimentation, holdouts, bandit optimization, and a rich evaluation envelope. Your flag keys and targeting rule logic migrate directly. The main changes are the SDK package names, a richer evaluation return type, and the addition of an event-tracking API that ConfigCat does not provide.

Concept mapping

ConfigCatA vs B
Feature flag (boolean)Boolean flag (getBoolFlag)
Text settingString flag (getStringFlag)
Number settingNumber flag (getNumberFlag)
JSON settingJSON flag (getJsonFlag<T>)
configcatClient.getValueAsync(key, default, user)server.forUser(ctx).getBoolFlag(key, default).value
configcatClient.getValueDetailsAsync(key, default, user)server.forUser(ctx).getFlag(key, default) — always full envelope
EvaluationDetails.valueFlag<T>.value
EvaluationDetails.variationIdFlag<T>.variationKey (string key, not numeric)
EvaluationDetails.matchedEvaluationRuleFlag<T>.ruleId + .ruleType
configcatClient.getAllValuesAsync(user)client.getAllFlags()
ConfigCatUserEvalContext (SingleContext)
user.identifiercontext.key
user.emailcontext.email (flat attribute)
user.countrycontext.country (flat attribute)
user.customTop-level attributes on EvalContext
No built-in event trackingclient.track(eventKey, { value?, properties? })
Polling / Lazy loading / Auto pollConfigurable polling interval (default 60 s)
client.dispose()client.close()

Client construction

ConfigCat offers multiple polling modes (auto poll, lazy load, manual poll). A vs B uses a single configurable polling interval with optional SSE streaming for the browser. The SDK key maps directly.

ConfigCat — Node
ts
1import * as configcat from 'configcat-node';
2
3const client = configcat.getClient(
4 'YOUR-SDK-KEY',
5 configcat.PollingMode.AutoPoll,
6 { pollIntervalSeconds: 60 }
7);
A vs B — Node
ts
1import { AvsbServer } from '@avsbhq/node';
2
3const server = new AvsbServer({
4 sdkKey: process.env.AVSB_SDK_KEY!,
5 pollingInterval: 60_000, // ms; default is 60 000
6});
7const result = await server.onReady();
ConfigCat — Browser (configcat-js)
ts
1import * as configcat from 'configcat-js';
2
3const client = configcat.getClient('YOUR-SDK-KEY');
A vs B — Browser
ts
1import { AvsbClient } from '@avsbhq/browser';
2
3const client = new AvsbClient({
4 sdkKey: 'sdk_production_abc123',
5 context: { kind: 'user', key: 'u_123', email: 'user@example.com' },
6});
7await client.onReady();
Info
ConfigCat's lazy loading mode fetches the config only on the first evaluation. A vs B always eagerly fetches on construction. Use the bootstrap prop in SSR scenarios to avoid any loading delay.

Identity model

ConfigCat passes a ConfigCatUser on each evaluation call. A vs B follows the same per-call pattern on the server via forUser, and binds a mutable context on the browser client.

ConfigCat
ts
1import { ConfigCatUser } from 'configcat-node';
2
3const user = new ConfigCatUser(
4 'u_123', // identifier
5 'user@ex.com', // email
6 'US', // country
7 { plan: 'pro' } // custom attributes
8);
9const value = await client.getValueAsync('show_banner', false, user);
A vs B
ts
1// Server
2const ctx = {
3 kind: 'user',
4 key: 'u_123',
5 email: 'user@ex.com',
6 country: 'US',
7 plan: 'pro',
8};
9const flag = server.forUser(ctx).getBoolFlag('show_banner', false);
10
11// Browser — context bound at construction, update via updateAttributes
12client.updateAttributes({ plan: 'enterprise' });
13
14// Stitch anonymous → identified identity at login
15await client.alias(
16 { kind: 'user', key: 'anon_xyz' },
17 { kind: 'user', key: 'u_123' }
18);

Flag evaluation

ConfigCat's primary API is getValueAsync (returns just the value) and getValueDetailsAsync (returns value plus evaluation metadata). A vs B always returns the full Flag<T>— there is no separate “details” call.

ConfigCat
ts
1// Simple value
2const showBanner = await client.getValueAsync('show_banner', false, user);
3
4// With evaluation details
5const details = await client.getValueDetailsAsync('show_banner', false, user);
6// details.value, details.variationId, details.matchedEvaluationRule
7
8// All values
9const all = await client.getAllValuesAsync(user);
A vs B
ts
1const uc = server.forUser(ctx);
2
3// Boolean
4const flag = uc.getBoolFlag('show_banner', false);
5flag.value // boolean
6flag.isEnabled() // source === 'rule' && value is truthy
7flag.variationKey // string key or null
8flag.ruleId // matched rule id or null
9flag.source // 'rule' | 'default' | 'holdout' | 'sticky' | ...
10flag.reasons // string[]
11
12// String
13const theme = uc.getStringFlag('homepage_theme', 'default');
14
15// Number
16const timeout = uc.getNumberFlag('api_timeout', 30);
17
18// JSON
19interface PricingConfig { basePrice: number; currency: string }
20const pricing = uc.getJsonFlag<PricingConfig>('pricing_config', { basePrice: 99, currency: 'USD' });
21
22// All flags (synchronous, no exposures by default)
23const all = client.getAllFlags();
Tip
ConfigCat's getValueAsynccalls are async because of lazy-load polling. A vs B's evaluation calls are synchronous after onReady() resolves — the datafile is fully in memory. Remove await from your evaluation call sites.

Tracking events

ConfigCat does not have a built-in event tracking API. A vs B adds full event tracking for A/B test metric collection.

A vs B — event tracking (new capability)
ts
1// Browser
2client.track('purchase_completed', {
3 value: 49.99,
4 properties: { orderId: 'ord_99', plan: 'pro' },
5});
6
7// Server
8server.track('purchase_completed', {
9 context: { kind: 'user', key: 'u_123' },
10 value: 49.99,
11 properties: { orderId: 'ord_99' },
12});
Info
Tracking is required only when you use A/B test rules with metric targets. If you are migrating a pure feature-flag setup (no experiments), you can add track calls incrementally as you set up A/B tests.

Multi-context

ConfigCat targets on a single user context. A vs B adds multi-context targeting, which is a new capability you gain on migration. You can now bucket and target simultaneously on user attributes, organization attributes, device type, and any other entity kind you define.

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

ConfigCat's auto-poll mode refreshes the config on a timer. A vs B uses the same approach with an optional SSE stream for the browser client. Subscribe to the configUpdate event to react to datafile refreshes in real time.

ConfigCat — config change listener
ts
1client.on('configChanged', (config) => {
2 // Config refreshed
3});
A vs B — config update listener
ts
1client.on('configUpdate', ({ publishedAt, reason }) => {
2 // reason: 'poll' | 'stream' | 'manual'
3});
4
5client.on('flagChange', ({ flagKey, previousValue, newValue }) => {
6 // A specific flag changed
7});

Bootstrap / SSR

ConfigCat does not have a built-in bootstrap mechanism for SSR. A vs B supports server-side datafile pre-fetch and a bootstrap prop that eliminates the loading flash entirely.

A vs B — SSR bootstrap
ts
1// Server component
2import { fetchDatafile } from '@avsbhq/node/server';
3const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);
4
5// Client provider
6<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}>
7 {children}
8</AvsbProvider>

Sticky bucketing

ConfigCat does not offer sticky bucketing natively. A vs B provides a StickyBucketService interface — implement it to ensure users remain in the same variation for the lifetime of an A/B test regardless of audience rule changes.

A vs B — sticky bucket service
ts
1import { StickyBucketService, StickyAssignment } from '@avsbhq/core';
2
3class LocalStorageStickyService implements StickyBucketService {
4 lookup(userId: string, flagKey: string): StickyAssignment | null {
5 const raw = localStorage.getItem(`avsb_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(`avsb_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 LocalStorageStickyService(),
17});

Holdouts

ConfigCat does not have a holdout concept. A vs B adds first-class holdout support on all plans. After migration you can create a holdout in the dashboard and enroll flags — held-out users are automatically served the control variation and flagged with source: 'holdout' in evaluation results.

Cleanup

ConfigCat
ts
1client.dispose();
A vs B
ts
1await client.close(); // flushes events and stops polling

Testing

ConfigCat — test client
ts
1import * as configcat from 'configcat-node';
2// Use ManualPollOptions + forceRefresh for test control
3const client = configcat.getClient('key', configcat.PollingMode.ManualPoll);
A vs B — mock client
ts
1import { createMockClient } from '@avsbhq/test';
2
3const mock = createMockClient({
4 flags: {
5 show_banner: false,
6 homepage_theme: 'blue',
7 api_timeout: 15,
8 pricing_config: { basePrice: 49, currency: 'USD' },
9 },
10});

Cutover checklist

1

Remove ConfigCat packages

Uninstall configcat-node, configcat-js, and configcat-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 ConfigCat SDK key with your A vs B SDK key from the Environments page.
4

Migrate ConfigCatUser to EvalContext

Map identifier → key, set kind: 'user', and flatten email, country, and custom properties to top-level context attributes.
5

Replace getValueAsync calls

Replace await client.getValueAsync(key, default, user) with the appropriate synchronous typed evaluator: getBoolFlag, getStringFlag, getNumberFlag, or getJsonFlag<T>. Remove the await — evaluations are now synchronous.
6

Replace getValueDetailsAsync calls

Replace await client.getValueDetailsAsync(key, default, user) with server.forUser(ctx).getFlag(key, default) — the full Flag<T> envelope is always returned.
7

Add event tracking

Instrument conversion events with client.track(eventKey, { value, properties }) at points where you want to measure experiment impact.
8

Update tests

Replace manual poll / forceRefresh patterns with createMockClient from @avsbhq/test.
9

Verify and deploy

Run npm run build and npx tsc --noEmit. Confirm flags resolve correctly in a staging environment before promoting to production.