From PostHog

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 (a hyphen in a key becomes an underscore: new-checkout becomes new_checkout)
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):

TypeScript
import posthog from 'posthog-js';posthog.init('phc_YOUR_KEY', {  api_host: 'https://app.posthog.com',});
TypeScript5 lines

A vs B, Browser:

TypeScript
import { AvsbClient } from '@avsbhq/browser';const client = new AvsbClient({  sdkKey: 'sdk_production_xxxxxxxxxxxxxxxx',  context: { kind: 'user', key: posthog.get_distinct_id() },});await client.onReady();
TypeScript7 lines

PostHog, Node (posthog-node):

TypeScript
import { PostHog } from 'posthog-node';const posthog = new PostHog('phc_YOUR_KEY', { host: 'https://app.posthog.com' });
TypeScript3 lines

A vs B, Node:

TypeScript
import { AvsbServer } from '@avsbhq/node';const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! });const result = await server.onReady();
TypeScript4 lines
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:

TypeScript
// Identify after loginposthog.identify('user_123', {  email: 'user@example.com',  plan: 'pro',  company: 'Acme',});// Partial update: re-call identify with all properties
TypeScript8 lines

A vs B:

TypeScript
// Full context replacement (equivalent to PostHog identify)client.identify({  kind: 'user',  key: 'user_123',  email: 'user@example.com',  plan: 'pro',  company: 'Acme',});// Partial attribute patch: no need to repeat unchanged fieldsclient.updateAttributes({ plan: 'enterprise' });// Stitch anonymous pre-login identity to the identified user. Synchronous:// it records the moment, it does not move past decisions.client.alias(  { kind: 'user', key: posthog.get_distinct_id() },  // anonymous id  { kind: 'user', key: 'user_123' }                   // identified id);
TypeScript18 lines

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:

TypeScript
// Boolean checkconst showBanner = posthog.isFeatureEnabled('show_banner');// Multivariate / string variantconst theme = posthog.getFeatureFlag('homepage_theme'); // returns string | boolean | undefined// JSON payloadconst config = posthog.getFeatureFlagPayload('pricing_config'); // returns JsonType | null
TypeScript8 lines

A vs B, Browser:

TypeScript
// Boolean flagconst showBanner = client.getBoolFlag('show_banner', false).value;// Or use isEnabled(): true only when a real decision was made (a rule,// holdout, bandit, or override matched, not just the default) and the// value is truthy.const enabled = client.getBoolFlag('show_banner', false).isEnabled();// String / multivariate flagconst theme = client.getStringFlag('homepage_theme', 'default').value;// JSON payload. Unlike PostHog's `JsonType | null`, the default you pass is// the type you get back, so there is no null branch at the call site.interface PricingConfig { basePrice: number; currency: string }const config = client.getJsonFlag<PricingConfig>('pricing_config', {  basePrice: 99,  currency: 'USD',}).value;// Full envelope (equivalent to fetching metadata)const flag = client.getBoolFlag('show_banner', false);flag.variationKey   // 'test' | 'control' | nullflag.source         // 'rule' | 'default' | 'holdout' | ...flag.reasons        // string[]
TypeScript23 lines

PostHog, Node:

TypeScript
// Requires a distinct ID per call (no client-level binding)const flagValue = await posthog.getFeatureFlag('show_banner', 'user_123');const isEnabled = await posthog.isFeatureEnabled('show_banner', 'user_123');
TypeScript3 lines

A vs B, Node:

TypeScript
const flag = server.forUser({ kind: 'user', key: 'user_123' })  .getBoolFlag('show_banner', false);
TypeScript2 lines

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:

TypeScript
posthog.capture('purchase_completed', {  revenue: 49.99,  orderId: 'ord_99',  plan: 'pro',});
TypeScript5 lines

A vs B:

TypeScript
// Sends to A vs B experiment analytics only. `revenue` is money in major// units (49.99, not 4999). Conversion events do not store properties// (only exposure events do), so orderId and plan have no A vs B// equivalent on this call.client.track('purchase_completed', { revenue: 49.99 });// If keeping PostHog for analytics, also call:// posthog.capture('purchase_completed', { revenue: 49.99, ... });
TypeScript8 lines

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

TypeScript
import type { MultiContext } from '@avsbhq/core';const ctx: MultiContext = {  kind: 'multi',  user:         { kind: 'user',         key: 'user_123', plan: 'pro' },  organization: { kind: 'organization', key: 'org_42',   tier: 'enterprise' },};server.forUser(ctx).getBoolFlag('enterprise_feature', false);
TypeScript8 lines

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:

TypeScript
posthog.onFeatureFlags((flags: string[]) => {  // Flags loaded or reloaded});
TypeScript3 lines

A vs B:

TypeScript
client.on('configUpdate', ({ reason }) => {  // Datafile refreshed; reason: 'poll' | 'stream' | 'manual'});client.on('flagChange', ({ flagKey, previousValue, newValue }) => {  // A specific flag value changed});// Manual refresh (equivalent to reloadFeatureFlags)await client.refresh();
TypeScript10 lines

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:

TypeScript
// Server componentconst flagValue = await posthog.isFeatureEnabled('show_banner', 'user_123');
TypeScript2 lines

A vs B, Next.js bootstrap:

TypeScript React
// Server component (app/layout.tsx)import { AvsbProvider } from '@avsbhq/react';import { fetchDatafile } from '@avsbhq/browser/server';const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);// Client provider: flags evaluate synchronously on first render<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}>  <YourApp /></AvsbProvider>
TypeScript React10 lines

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:

TypeScript
await posthog.shutdown();
TypeScript1 line

A vs B:

TypeScript
await server.close(); // flushes buffered events, stops polling
TypeScript1 line

Testing

PostHog, test override:

TypeScript
// PostHog test mode: use overrideFeatureFlagposthog.featureFlags.override({ show_banner: true });
TypeScript2 lines

A vs B, mock client:

TypeScript
import { createMockClient } from '@avsbhq/test';const mock = createMockClient({  flags: {    show_banner: true,    homepage_theme: 'blue',  },});
TypeScript8 lines

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.

Was this helpful?