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
| ConfigCat | A vs B |
|---|---|
| Feature flag (boolean) | Boolean flag (getBoolFlag) |
| Text setting | String flag (getStringFlag) |
| Number setting | Number flag (getNumberFlag) |
| JSON setting | JSON 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.value | Flag<T>.value |
EvaluationDetails.variationId | Flag<T>.variationKey (string key, not numeric) |
EvaluationDetails.matchedEvaluationRule | Flag<T>.ruleId + .ruleType |
configcatClient.getAllValuesAsync(user) | client.getAllFlags() |
ConfigCatUser | EvalContext (SingleContext) |
user.identifier | context.key |
user.email | context.email (flat attribute) |
user.country | context.country (flat attribute) |
user.custom | Top-level attributes on EvalContext |
| No built-in event tracking | client.track(eventKey, { value?, properties? }) |
| Polling / Lazy loading / Auto poll | Configurable 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.
1import * as configcat from 'configcat-node';2
3const client = configcat.getClient(4 'YOUR-SDK-KEY',5 configcat.PollingMode.AutoPoll,6 { pollIntervalSeconds: 60 }7);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 0006});7const result = await server.onReady();1import * as configcat from 'configcat-js';2
3const client = configcat.getClient('YOUR-SDK-KEY');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();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.
1import { ConfigCatUser } from 'configcat-node';2
3const user = new ConfigCatUser(4 'u_123', // identifier5 'user@ex.com', // email6 'US', // country7 { plan: 'pro' } // custom attributes8);9const value = await client.getValueAsync('show_banner', false, user);1// Server2const 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 updateAttributes12client.updateAttributes({ plan: 'enterprise' });13
14// Stitch anonymous → identified identity at login15await 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.
1// Simple value2const showBanner = await client.getValueAsync('show_banner', false, user);3
4// With evaluation details5const details = await client.getValueDetailsAsync('show_banner', false, user);6// details.value, details.variationId, details.matchedEvaluationRule7
8// All values9const all = await client.getAllValuesAsync(user);1const uc = server.forUser(ctx);2
3// Boolean4const flag = uc.getBoolFlag('show_banner', false);5flag.value // boolean6flag.isEnabled() // source === 'rule' && value is truthy7flag.variationKey // string key or null8flag.ruleId // matched rule id or null9flag.source // 'rule' | 'default' | 'holdout' | 'sticky' | ...10flag.reasons // string[]11
12// String13const theme = uc.getStringFlag('homepage_theme', 'default');14
15// Number16const timeout = uc.getNumberFlag('api_timeout', 30);17
18// JSON19interface 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();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.
1// Browser2client.track('purchase_completed', {3 value: 49.99,4 properties: { orderId: 'ord_99', plan: 'pro' },5});6
7// Server8server.track('purchase_completed', {9 context: { kind: 'user', key: 'u_123' },10 value: 49.99,11 properties: { orderId: 'ord_99' },12});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.
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.
1client.on('configChanged', (config) => {2 // Config refreshed3});1client.on('configUpdate', ({ publishedAt, reason }) => {2 // reason: 'poll' | 'stream' | 'manual'3});4
5client.on('flagChange', ({ flagKey, previousValue, newValue }) => {6 // A specific flag changed7});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.
1// Server component2import { fetchDatafile } from '@avsbhq/node/server';3const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);4
5// Client provider6<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.
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
1client.dispose();1await client.close(); // flushes events and stops pollingTesting
1import * as configcat from 'configcat-node';2// Use ManualPollOptions + forceRefresh for test control3const client = configcat.getClient('key', configcat.PollingMode.ManualPoll);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
Remove ConfigCat packages
configcat-node, configcat-js, and configcat-react from your project.Install A vs B packages
@avsbhq/node, @avsbhq/browser, and @avsbhq/react as needed.Swap SDK key
Migrate ConfigCatUser to EvalContext
identifier → key, set kind: 'user', and flatten email, country, and custom properties to top-level context attributes.Replace getValueAsync calls
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.Replace getValueDetailsAsync calls
await client.getValueDetailsAsync(key, default, user) with server.forUser(ctx).getFlag(key, default) — the full Flag<T> envelope is always returned.Add event tracking
client.track(eventKey, { value, properties }) at points where you want to measure experiment impact.Update tests
createMockClient from @avsbhq/test.Verify and deploy
npm run build and npx tsc --noEmit. Confirm flags resolve correctly in a staging environment before promoting to production.