From ConfigCat

A feature flag is a switch in your code that turns something on or off without a new deploy. ConfigCat is a feature flag service: it reads typed values and applies simple targeting rules, conditions that decide which value a visitor gets.

A vs B does everything ConfigCat does, and more. It adds A/B test experimentation, bandit optimization, and a fuller evaluation result. A holdout is a group of visitors held back from every test, so you can measure the total impact of everything you shipped. A vs B also adds event tracking, which ConfigCat does not have.

Your targeting rules move over as they are, and so do flag keys that use only lowercase letters, digits and underscores. A ConfigCat key in camelCase, such as isNewCheckoutEnabled, becomes is_new_checkout_enabled in A vs B. What changes: the SDK package names, a richer evaluation result, and the new tracking API.

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()
UserEvalContext (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:

TypeScript
import * as configcat from 'configcat-node';const configCatClient = configcat.getClient(  'YOUR-SDK-KEY',  configcat.PollingMode.AutoPoll,  { pollIntervalSeconds: 60 });
TypeScript7 lines

A vs B, Node:

TypeScript
import { AvsbServer } from '@avsbhq/node';const server = new AvsbServer({  sdkKey: process.env.AVSB_SDK_KEY!,  pollingInterval: 60_000, // ms; default is 60 000});const result = await server.onReady();
TypeScript7 lines

ConfigCat, Browser (configcat-js):

TypeScript
import * as configcat from 'configcat-js';const configCatBrowserClient = configcat.getClient('YOUR-SDK-KEY');
TypeScript3 lines

A vs B, Browser:

TypeScript
import { AvsbClient } from '@avsbhq/browser';const client = new AvsbClient({  sdkKey: 'sdk_production_xxxxxxxxxxxxxxxx',  context: { kind: 'user', key: 'u_123', email: 'user@example.com' },});await client.onReady();
TypeScript7 lines
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 User object 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:

TypeScript
import { User } from 'configcat-node';const user = new User(  'u_123',          // identifier  'user@ex.com',    // email  'US',             // country  { plan: 'pro' }   // custom attributes);const value = await configCatClient.getValueAsync('show_banner', false, user);
TypeScript9 lines

A vs B:

TypeScript
// Serverconst ctx = {  kind: 'user',  key: 'u_123',  email: 'user@ex.com',  country: 'US',  plan: 'pro',};const flag = server.forUser(ctx).getBoolFlag('show_banner', false);// Browser: context bound at construction, update via updateAttributesclient.updateAttributes({ plan: 'enterprise' });// Stitch anonymous → identified identity at login. Synchronous: it records// the moment, it does not move past decisions.client.alias(  { kind: 'user', key: 'anon_xyz' },  { kind: 'user', key: 'u_123' });
TypeScript19 lines

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:

TypeScript
// Simple valueconst showBanner = await configCatClient.getValueAsync('show_banner', false, user);// With evaluation detailsconst details = await configCatClient.getValueDetailsAsync('show_banner', false, user);// details.value, details.variationId, details.matchedEvaluationRule// All valuesconst all = await configCatClient.getAllValuesAsync(user);
TypeScript9 lines

A vs B:

TypeScript
const uc = server.forUser(ctx);// Booleanconst flag = uc.getBoolFlag('show_banner', false);flag.value        // booleanflag.isEnabled()  // true only for a real decision (rule, holdout, bandit, or override) with a truthy valueflag.variationKey // string key or nullflag.ruleId       // matched rule id or nullflag.source       // 'rule' | 'default' | 'holdout' | 'sticky' | ...flag.reasons      // string[]// Stringconst theme = uc.getStringFlag('homepage_theme', 'default');// Numberconst timeout = uc.getNumberFlag('api_timeout', 30);// JSONinterface PricingConfig { basePrice: number; currency: string }const pricing = uc.getJsonFlag<PricingConfig>('pricing_config', { basePrice: 99, currency: 'USD' });// All flags (synchronous, no exposures by default)const all = client.getAllFlags();
TypeScript23 lines
Tip

ConfigCat's getValueAsync calls are async because of lazy-load polling. A vs B's evaluation calls are synchronous after onReady() resolves, since 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). revenue is money in major units (49.99, not 4999); value is a separate numeric-metric column for things like items in a cart:

TypeScript
// Browserclient.track('purchase_completed', { revenue: 49.99 });// Server: pass the context explicitly, since there is no bound clientserver.track('purchase_completed', {  context: { kind: 'user', key: 'u_123' },  revenue: 49.99,});
TypeScript8 lines
Warning

track() also accepts a properties object. Conversion events do not store it today; only exposure events do. The SDK logs a warning and drops it rather than pretending to save it. Segmenting a conversion by a custom property is not available yet.

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):

TypeScript
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);
TypeScript8 lines

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:

TypeScript
configCatClient.on('configChanged', (config: unknown) => {  // Config refreshed});
TypeScript3 lines

A vs B, config update listener:

TypeScript
client.on('configUpdate', ({ publishedAt, reason }) => {  // reason: 'poll' | 'stream' | 'manual'});client.on('flagChange', ({ flagKey, previousValue, newValue }) => {  // A specific flag changed});
TypeScript7 lines

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:

TypeScript React
// Server componentimport { AvsbProvider } from '@avsbhq/react';import { fetchDatafile } from '@avsbhq/browser/server';const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);// Client provider<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}>  <YourApp /></AvsbProvider>
TypeScript React10 lines

Sticky bucketing

ConfigCat does not offer sticky bucketing natively. Sticky bucketing keeps a visitor in the same variation for the life of an A/B test, even if you change its targeting rules later. A vs B provides a StickyBucketService interface on the server SDK (@avsbhq/node) that does this. Wire one in: InMemoryStickyBucketService and RedisStickyBucketService ship with the package, or implement the two-method interface over your own store.

A vs B, sticky bucket service:

TypeScript
import type { StickyBucketService, StickyAssignment } from '@avsbhq/core';class MyStickyService implements StickyBucketService {  // Both methods are synchronous, so back them with a warm local cache.  private cache = new Map<string, StickyAssignment>();  lookup(userId: string, flagKey: string): StickyAssignment | null {    return this.cache.get(`avsb_sticky_${userId}_${flagKey}`) ?? null;  }  save(userId: string, flagKey: string, assignment: StickyAssignment): void {    this.cache.set(`avsb_sticky_${userId}_${flagKey}`, assignment);  }}const stickyServer = new AvsbServer({  sdkKey: process.env.AVSB_SDK_KEY!,  stickyBucketService: new MyStickyService(),});
TypeScript18 lines

Holdouts

ConfigCat does not have a holdout concept. A vs B adds first-class holdout support on all plans. Create a holdout in the dashboard and enroll flags in it. Held-out visitors are automatically served the control variation, and flagged with source: 'holdout' in evaluation results.

Cleanup

ConfigCat:

TypeScript
configCatClient.dispose();
TypeScript1 line

A vs B:

TypeScript
await client.close(); // flushes events and stops polling
TypeScript1 line

Testing

ConfigCat, test client:

TypeScript
import * as configcat from 'configcat-node';// Use ManualPollOptions + forceRefresh for test controlconst client = configcat.getClient('key', configcat.PollingMode.ManualPoll);
TypeScript3 lines

A vs B, mock client:

TypeScript
import { createMockClient } from '@avsbhq/test';const mock = createMockClient({  flags: {    show_banner: false,    homepage_theme: 'blue',    api_timeout: 15,    pricing_config: { basePrice: 49, currency: 'USD' },  },});
TypeScript10 lines

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 in the sidebar.

  1. Environments lives in the sidebar on its own, not inside Settings.
  2. Click Reveal to see the full key, then Copy to copy it.
4

Migrate User 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.

Was this helpful?