PostHog

Use PostHog feature flags with the Flags SDK.

The PostHog package provides a managed PostHog adapter for the Flags SDK.

Learn more about Adapters

Deploy the template

Example usage

flags.ts
import { flag } from "flags/next";
import { postHogAdapter } from '@flags-sdk/posthog'
import identify from "@/lib/identify";

// Reads the flag's evaluated value. Pass the adapter uninvoked
// (`postHogAdapter`) or invoked (`postHogAdapter()`) — both work.
export const myFlag = flag<boolean>({
  key: "posthog-flag",
  adapter: postHogAdapter,
  identify,
});

export const myFlagVariant = flag<string>({
  key: "posthog-multivariate-flag",
  adapter: postHogAdapter,
  identify,
});

// Reads the flag's attached payload with `.payload`.
export const myFlagPayload = flag({
  key: "posthog-flag-with-payload",
  adapter: postHogAdapter.payload,
  defaultValue: {},
  identify,
});

Getting started

Install the required dependencies:

pnpm i @flags-sdk/posthog

Environment variables

Always required, read by postHogAdapter:

.env.local
# The regional API host, which determines where your data lives
# Settings > Project > Project API Key
POSTHOG_HOST=https://us.i.posthog.com
# or https://eu.i.posthog.com

# Your project API key
# Settings > Project > Project API Key
POSTHOG_PROJECT_API_KEY=phc_...

Optional, opts postHogAdapter into local evaluation, where flag definitions are polled in the background instead of evaluating each flag remotely:

.env.local
# Settings > Project > Feature flags secret key
POSTHOG_SECRET_KEY=phs_...

For the Flags Explorer, read by getProviderData only:

.env.local
# Choose a project secret API key with Feature flag > Read access
POSTHOG_PROJECT_SECRET_API_KEY=phs_...
# Or use a personal API key with feature_flag:read scope
# POSTHOG_PERSONAL_API_KEY=phx_...
# Settings > Project > Project ID
POSTHOG_PROJECT_ID=521742

Import the PostHog adapter for Flags SDK, which reads POSTHOG_PROJECT_API_KEY, POSTHOG_HOST and POSTHOG_SECRET_KEY when it is first used:

flags.ts
import { postHogAdapter } from '@flags-sdk/posthog'

If needed, you can instead initialize the adapter with your own options by importing createPostHogAdapter

flags.ts
import { createPostHogAdapter } from '@flags-sdk/posthog'

const postHogAdapter = createPostHogAdapter({
  postHogKey: process.env.POSTHOG_PROJECT_API_KEY!,
  postHogOptions: {
    host: process.env.POSTHOG_HOST,
    // ...
  },
})

The postHogAdapter is a single callable adapter. You can pass it directly, or invoke it — both are equivalent:

  • postHogAdapter (or postHogAdapter()): resolves the flag's evaluated value. For a boolean flag this is a boolean; for a multivariate flag it is the variant string. Type the flag (e.g. flag<boolean>) to get the value type you expect.
  • postHogAdapter.payload (or postHogAdapter.payload()): resolves the flag's attached payload.

The flag's key is used as the PostHog feature flag key as-is. Every flag needs an identify function returning the entities the adapter evaluates against:

lib/identify.ts
import type { Identify } from 'flags'
import type { PostHogEntities } from '@flags-sdk/posthog'

export const identify: Identify<PostHogEntities> = async () => {
  return { distinctId: 'user-123' }
}

The adapter throws if entities is missing, so a flag without identify will fail at evaluation time.

app/flags.ts
import { flag } from 'flags/next'
import { postHogAdapter } from '@flags-sdk/posthog'
import { identify } from '@/lib/identify'

export const exampleFlag = flag<boolean>({
  key: 'example-flag',
  defaultValue: false,
  adapter: postHogAdapter,
  identify,
})

Flags backed by this adapter participate in bulk evaluation: evaluate() resolves flags that share an identify source through a single PostHog request.

Then use it in your framework:

app/page.tsx
import { exampleFlag } from "@/flags";

export default async function Page() {
  const exampleValue = await exampleFlag();

  return <div>Example Flag: {String(exampleValue)}</div>;
}

Evaluation modes

PostHog can evaluate flags remotely or locally.

Remote evaluation (default)

With only POSTHOG_PROJECT_API_KEY and POSTHOG_HOST set, the adapter evaluates remotely. PostHog makes a network request for every feature flag evaluation, and each request is billed separately. Having to make a network request for every flag evaluation also adds latency. If you provide user ids, PostHog will look up additional properties from its database.

Local evaluation

Setting POSTHOG_SECRET_KEY (phs_...) is the only thing that switches evaluation modes — the default adapter enables local evaluation exactly when that key is present.

In this mode, posthog-node periodically fetches your flag definitions in the background (every 30s by default) and evaluates flags in-process against those cached definitions, avoiding a network round trip on most checks after initialization. Because evaluation happens locally, you're responsible for providing every property the flag's release conditions depend on.

Each background poll is billed as 10 flag requests, independent of how many checks it serves, so for a long-running process this is usually far cheaper than paying per check. But the poller runs per compute instance, so PostHog recommends against local evaluation in short-lived compute, where each invocation would otherwise start its own poller and multiply cost rather than amortize it. Use remote evaluation there instead. You can widen the polling interval to trade slower propagation of flag changes for lower polling cost.

With Fluid Compute you may see latency and cost benefits from using local evaluation. It depends on your traffic patterns and load.

You can also enable it explicitly with createPostHogAdapter:

flags.ts
import { createPostHogAdapter } from '@flags-sdk/posthog'

const postHogAdapter = createPostHogAdapter({
  postHogKey: process.env.POSTHOG_PROJECT_API_KEY!,
  postHogOptions: {
    host: process.env.POSTHOG_HOST,
    secretKey: process.env.POSTHOG_SECRET_KEY,
    enableLocalEvaluation: true,
  },
})

POSTHOG_PERSONAL_API_KEY and POSTHOG_PROJECT_SECRET_API_KEY are used only by the Flags Explorer (getProviderData, below) to discover flag definitions. They are never passed to the runtime client and do not enable local evaluation.

The default adapter also sets disableGeoip: true, since the server's IP is not a good proxy for the user's location. Use createPostHogAdapter if you want the GeoIP-derived person properties.

Flags Explorer

How to inform the Flags Explorer about flags

You can view and override these flags using the Flags Explorer

getProviderData supports either a project secret API key or a personal API key, plus your project ID. Existing personal API key configurations continue to work.

Project secret API key

Create a project secret API key (phs_...) in your PostHog project's settings. Only feature_flag:read is required for the project-scoped token. In the permissions editor, set Feature flag to Read and every other scope to No access, including Endpoint, Account, Loop, and Experiment. No write access is needed. The definitions endpoint checks the feature flag scope; experiment variants and payloads are included without an additional Experiment permission.

.env.local
POSTHOG_PROJECT_SECRET_API_KEY=phs_...
POSTHOG_PROJECT_ID=521742
app/.well-known/vercel/flags/route.ts
app/.well-known/vercel/flags/route.ts
import { createFlagsDiscoveryEndpoint } from 'flags/next'
import { getProviderData as getPostHogProviderData } from '@flags-sdk/posthog'

export const GET = createFlagsDiscoveryEndpoint(() => getPostHogProviderData({
  projectSecretApiKey: process.env.POSTHOG_PROJECT_SECRET_API_KEY!,
  projectId: process.env.POSTHOG_PROJECT_ID!,
}))

This mode fetches /flags/definitions/ from the ingestion host configured by POSTHOG_HOST (for example, https://us.i.posthog.com). The key identifies the project for authentication; projectId supplies links to flags in the PostHog dashboard. Pass apiHost to override the ingestion host and appHost to override the dashboard host for a self-hosted or proxied instance.

The response includes descriptions from PostHog's name field and available flag options. Nonempty payloads are used as options; otherwise the adapter exposes boolean values or multivariate variants. This endpoint does not return creation timestamps.

Personal API key

Alternatively, create a personal API key with Feature flag > Read (feature_flag:read) and access to the intended project.

.env.local
# Settings > User > Personal API keys
POSTHOG_PERSONAL_API_KEY=phx_...
# Settings > Project > Project ID
POSTHOG_PROJECT_ID=521742
app/.well-known/vercel/flags/route.ts
app/.well-known/vercel/flags/route.ts
import { createFlagsDiscoveryEndpoint } from 'flags/next'
import { getProviderData as getPostHogProviderData } from '@flags-sdk/posthog'

export const GET = createFlagsDiscoveryEndpoint(() => getPostHogProviderData({
  personalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!,
  projectId: process.env.POSTHOG_PROJECT_ID!,
}))

Personal API key mode calls PostHog's management API on the app host, which it derives from POSTHOG_HOST (https://us.i.posthog.com becomes https://us.posthog.com, and the EU host maps accordingly). Pass appHost explicitly if you use a self-hosted or proxied instance. Missing credentials or host are reported back as hints in the Flags Explorer rather than throwing.

Learn more about the Flags Explorer

Additional resources