/Docs

Migrate from PostHog Feature Flags to A vs B

PostHog is a product analytics platform that ships feature flags as one of several built-in tools. If you are migrating only your feature flag and A/B test layer — keeping PostHog for analytics — A vs B slots in alongside it. If you are replacing PostHog entirely, A vs B covers feature flags and experiment analysis; you will need a separate product analytics tool for session recording and funnels. PostHog's feature flag API surface is compact, so the migration is small: swap three methods and add an event-context object.

Concept mapping

PostHog Feature FlagsA vs B
Feature flagFeature flag (keys transfer directly)
posthog.getFeatureFlag(key)client.getFlag(key, null).value
posthog.isFeatureEnabled(key)client.getBoolFlag(key, false).isEnabled()
posthog.getFeatureFlagPayload(key)client.getJsonFlag<T>(key, null).value
String / multivariate flagString flag (getStringFlag)
posthog.identify(userId, properties)client.identify({ kind: 'user', key: userId, ...properties })
posthog.capture(eventName, properties)client.track(eventKey, { properties })
posthog.reloadFeatureFlags()client.refresh() (manual datafile refresh)
PostHog distinct IDcontext.key
Person propertiesTop-level attributes on EvalContext
No holdout conceptFlagDatafileHoldout — available on all plans
No multi-contextMultiContext — new capability gained on migration
posthog.reset()client.identify({ kind: 'user', key: 'anon_new' })

Client construction

PostHog initializes via a global posthog.init call with the API key and host. A vs B uses an explicit client instance with an SDK key.

PostHog — Browser (posthog-js)
ts
1import posthog from 'posthog-js';
2
3posthog.init('phc_YOUR_KEY', {
4 api_host: 'https://app.posthog.com',
5});
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: posthog.get_distinct_id() },
6});
7await client.onReady();
PostHog — Node (posthog-node)
ts
1import { PostHog } from 'posthog-node';
2
3const posthog = new PostHog('phc_YOUR_KEY', { host: 'https://app.posthog.com' });
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();
Info
PostHog's feature flags work with a project API key that also drives analytics. A vs B uses a separate per-environment SDK key that is safe to expose in browser code. Find it on the Environments page in your feature flag project.

Identity model

PostHog uses posthog.identify(distinctId, properties) to associate a user and their properties with the PostHog distinct ID. A vs B maps this directly to client.identify with a context object.

PostHog
ts
1// Identify after login
2posthog.identify('user_123', {
3 email: 'user@example.com',
4 plan: 'pro',
5 company: 'Acme',
6});
7
8// Partial update — re-call identify with all properties
A vs B
ts
1// Full context replacement (equivalent to PostHog identify)
2client.identify({
3 kind: 'user',
4 key: 'user_123',
5 email: 'user@example.com',
6 plan: 'pro',
7 company: 'Acme',
8});
9
10// Partial attribute patch — no need to repeat unchanged fields
11client.updateAttributes({ plan: 'enterprise' });
12
13// Stitch anonymous pre-login identity to the identified user
14await client.alias(
15 { kind: 'user', key: posthog.get_distinct_id() }, // anonymous id
16 { kind: 'user', key: 'user_123' } // identified id
17);

Flag evaluation

PostHog provides three flag evaluation methods. A vs B unifies them under typed evaluators that always return a Flag<T>envelope, so you get evaluation metadata for free without a separate “details” call.

PostHog — Browser
ts
1// Boolean check
2const showBanner = posthog.isFeatureEnabled('show_banner');
3
4// Multivariate / string variant
5const theme = posthog.getFeatureFlag('homepage_theme'); // returns string | boolean | undefined
6
7// JSON payload
8const config = posthog.getFeatureFlagPayload('pricing_config'); // returns JsonType | null
A vs B — Browser
ts
1// Boolean flag
2const showBanner = client.getBoolFlag('show_banner', false).value;
3// Or use isEnabled() which checks that a rule actually matched:
4const enabled = client.getBoolFlag('show_banner', false).isEnabled();
5
6// String / multivariate flag
7const theme = client.getStringFlag('homepage_theme', 'default').value;
8
9// JSON payload
10interface PricingConfig { basePrice: number; currency: string }
11const config = client.getJsonFlag<PricingConfig>('pricing_config', null).value;
12
13// Full envelope (equivalent to fetching metadata)
14const flag = client.getBoolFlag('show_banner', false);
15flag.variationKey // 'test' | 'control' | null
16flag.source // 'rule' | 'default' | 'holdout' | ...
17flag.reasons // string[]
PostHog — Node
ts
1// Requires a distinct ID per call (no client-level binding)
2const flagValue = await posthog.getFeatureFlag('show_banner', 'user_123');
3const isEnabled = await posthog.isFeatureEnabled('show_banner', 'user_123');
A vs B — Node
ts
1const flag = server.forUser({ kind: 'user', key: 'user_123' })
2 .getBoolFlag('show_banner', false);

Tracking events

PostHog uses posthog.capture for all analytics events. In A vs B, client.track sends conversion events to the experiment analytics pipeline. If you are keeping PostHog for analytics, you can call both — they operate independently.

PostHog
ts
1posthog.capture('purchase_completed', {
2 revenue: 49.99,
3 orderId: 'ord_99',
4 plan: 'pro',
5});
A vs B
ts
1// Sends to A vs B experiment analytics only
2client.track('purchase_completed', {
3 value: 49.99,
4 properties: { orderId: 'ord_99', plan: 'pro' },
5});
6
7// If keeping PostHog for analytics, also call:
8// posthog.capture('purchase_completed', { revenue: 49.99, ... });

Multi-context

PostHog targets feature flags on a single user identity. A vs B adds multi-context targeting — a new capability you gain on migration. You can target and bucket on organization, device, or any other entity kind alongside the user context.

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: 'user_123', plan: 'pro' },
6 organization: { kind: 'organization', key: 'org_42', tier: 'enterprise' },
7};
8server.forUser(ctx).getBoolFlag('enterprise_feature', false);

Streaming updates

PostHog reloads feature flags via posthog.reloadFeatureFlags() or on an internal timer. A vs B polls automatically at a configurable interval and emits events when the datafile changes.

PostHog
ts
1posthog.onFeatureFlags((flags) => {
2 // Flags loaded or reloaded
3});
A vs B
ts
1client.on('configUpdate', ({ reason }) => {
2 // Datafile refreshed — reason: 'poll' | 'stream' | 'manual'
3});
4
5client.on('flagChange', ({ flagKey, previousValue, newValue }) => {
6 // A specific flag value changed
7});
8
9// Manual refresh (equivalent to reloadFeatureFlags)
10await client.refresh();

Bootstrap / SSR

PostHog's server-side Node SDK evaluates flags per request without a bootstrap step. A vs B supports a full SSR bootstrap pattern that eliminates client-side loading state entirely.

PostHog — Next.js per-request eval
ts
1// Server component
2const flagValue = await posthog.isFeatureEnabled('show_banner', userId);
A vs B — Next.js bootstrap
ts
1// Server component (app/layout.tsx)
2import { fetchDatafile } from '@avsbhq/node/server';
3const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);
4
5// Client provider — flags evaluate synchronously on first render
6<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}>
7 {children}
8</AvsbProvider>

Holdouts

PostHog feature flags do not have a holdout concept. A vs B includes holdouts on all plans. Create a holdout in the dashboard and associate flags with it to maintain a clean control group across multiple concurrent experiments.

Cleanup

PostHog — Node
ts
1await posthog.shutdown();
A vs B
ts
1await server.close(); // flushes buffered events, stops polling

Testing

PostHog — test override
ts
1// PostHog test mode: use overrideFeatureFlag
2posthog.featureFlags.override({ show_banner: true });
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

Decide scope

Determine whether you are replacing only the feature flag layer (keeping PostHog for analytics) or replacing PostHog entirely. If keeping PostHog for analytics, keep posthog.capture calls in place and add client.track alongside them for experiment conversions.
2

Remove or reduce PostHog packages

If fully replacing: uninstall posthog-js and posthog-node. If keeping analytics: leave the packages but remove feature flag calls.
3

Install A vs B packages

Install @avsbhq/browser and/or @avsbhq/node and @avsbhq/react as needed.
4

Migrate identify calls

Replace posthog.identify(distinctId, properties) with client.identify({ kind: 'user', key: distinctId, ...properties }).
5

Replace flag evaluation calls

Replace posthog.isFeatureEnabled(key) with client.getBoolFlag(key, false).isEnabled(). Replace posthog.getFeatureFlag(key) with client.getStringFlag(key, null).value. Replace posthog.getFeatureFlagPayload(key) with client.getJsonFlag<T>(key, null).value.
6

Add conversion tracking

Add client.track(eventKey, { value, properties }) at each conversion point you want to measure in A/B test results.
7

Update tests

Replace posthog.featureFlags.override with createMockClient from @avsbhq/test.
8

Verify and deploy

Run npm run build and npx tsc --noEmit. Confirm flag evaluations and conversion events appear in the A vs B dashboard before promoting to production.