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
| 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() |
User | 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.
ConfigCat, Node:
import * as configcat from 'configcat-node';const configCatClient = configcat.getClient( 'YOUR-SDK-KEY', configcat.PollingMode.AutoPoll, { pollIntervalSeconds: 60 });A vs B, Node:
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();ConfigCat, Browser (configcat-js):
import * as configcat from 'configcat-js';const configCatBrowserClient = configcat.getClient('YOUR-SDK-KEY');A vs B, Browser:
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();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:
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);A vs B:
// 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' });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:
// 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);A vs B:
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();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:
// 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,});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.
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):
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);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:
configCatClient.on('configChanged', (config: unknown) => { // Config refreshed});A vs B, config update listener:
client.on('configUpdate', ({ publishedAt, reason }) => { // reason: 'poll' | 'stream' | 'manual'});client.on('flagChange', ({ flagKey, previousValue, newValue }) => { // A specific flag changed});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:
// 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>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:
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(),});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:
configCatClient.dispose();A vs B:
await client.close(); // flushes events and stops pollingTesting
ConfigCat, test client:
import * as configcat from 'configcat-node';// Use ManualPollOptions + forceRefresh for test controlconst client = configcat.getClient('key', configcat.PollingMode.ManualPoll);A vs B, mock client:
import { createMockClient } from '@avsbhq/test';const mock = createMockClient({ flags: { show_banner: false, homepage_theme: 'blue', api_timeout: 15, pricing_config: { basePrice: 49, currency: 'USD' }, },});Cutover checklist
Remove ConfigCat packages
Uninstall configcat-node, configcat-js, and configcat-react from your project.
Install A vs B packages
Install @avsbhq/node, @avsbhq/browser, and @avsbhq/react as needed.
Swap SDK key
Replace your ConfigCat SDK key with your A vs B SDK key from the Environments page in the sidebar.
- Environments lives in the sidebar on its own, not inside Settings.
- Click Reveal to see the full key, then Copy to copy it.
Migrate User to EvalContext
Map identifier → key, set kind: 'user', and flatten email, country, and custom properties to top-level context attributes.
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.
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.
Add event tracking
Instrument conversion events with client.track(eventKey, { value, properties }) at points where you want to measure experiment impact.
Update tests
Replace manual poll / forceRefresh patterns with createMockClient from @avsbhq/test.
Verify and deploy
Run npm run build and npx tsc --noEmit. Confirm flags resolve correctly in a staging environment before promoting to production.