Provider Adapters
Table of Contents
- Vercel
- Global Config
- Statsig
- LaunchDarkly
- PostHog
- GrowthBook
- Flagsmith
- Reflag
- OpenFeature
- Split
- Optimizely
- Custom Adapters
Vercel
Package: @flags-sdk/vercel
pnpm i flags @flags-sdk/vercelSetup
Prerequisite: The Vercel CLI must be installed and authenticated. If it is not installed, run
pnpm i -g vercel. For authentication or linking issues, follow thevercel-cliskill if it's already available. If it isn't, suggest the user install it withnpx skills add vercel/vercel@vercel-cli(don't auto-install it) and rely onvercel <command> --helpin the meantime.
The following steps integrate a flag into an app and need a linked project for vercel env pull; verify the link as described in Project targeting. To only inspect or change remote flags, follow CLI-only flag management instead.
- Create a flag in the Vercel dashboard or via CLI:
vercel flags create <flag-key> --kind boolean --description "<description>" - Pull env vars: run
vercel env pullto write the Vercel OIDC token and the DevelopmentFLAGS_SECRETto.env.local(Pull environment variables). See Authentication for SDK keys. - Declare the flag:
import { flag } from 'flags/next';
import { vercelAdapter } from '@flags-sdk/vercel';
export const exampleFlag = flag({
key: 'example-flag',
adapter: vercelAdapter,
});User targeting
import { dedupe, flag } from 'flags/next';
import { vercelAdapter } from '@flags-sdk/vercel';
type Entities = {
team?: { id: string };
user?: { id: string };
};
const identify = dedupe(async (): Promise<Entities> => ({
team: { id: 'team-123' },
user: { id: 'user-456' },
}));
export const exampleFlag = flag<boolean, Entities>({
key: 'example-flag',
identify,
adapter: vercelAdapter,
});Define the entities and attributes under Flags → Entities in the dashboard before you use them in rules or segments, and return the same names from identify (Entities). The example above allows --by user.id / --by team.id in vercel flags split, rollout, and rules add, and the matching conditions in dashboard rules. Entities are evaluated fresh on every call; a rule whose attribute is missing from the context is skipped. See How the CLI connects to the SDK.
Attribute types
Each attribute has a type: String, Number, Boolean, String Array, or Timestamp. The value identify returns must match it.
- Timestamp is a Unix epoch in milliseconds as a
number(Date.now(),user.createdAt.getTime(),1719792000000). Do not pass aDate, an ISO string, or seconds. - Timestamp rules only compare: is after / is before / is at or after / is at or before (
gt/lt/gte/lte). Equality, one-of, and exists operators are not available for Timestamp attributes. - Timestamp attributes cannot bucket a split or rollout (
--by); use them in rule conditions only. - To release at a certain time, pass the current time as
system.timeand compare against it in a rule. KeepDate.now()insidededupeso every flag in the request sees the same time:
const identify = dedupe(async () => ({
system: { time: Date.now() },
user: { id: 'user-456', signupAt: 1719792000000 },
}));Flags Explorer
import { createFlagsDiscoveryEndpoint } from 'flags/next';
import { getProviderData } from '@flags-sdk/vercel';
import * as flags from '../../../../flags';
export const GET = createFlagsDiscoveryEndpoint(async () => {
return await getProviderData(flags);
});Custom configuration
import { createVercelAdapter } from '@flags-sdk/vercel';
const customAdapter = createVercelAdapter(process.env.CUSTOM_FLAGS_KEY!);
export const exampleFlag = flag({
key: 'example-flag',
adapter: customAdapter,
});Using your own client (e.g. for singleton)
If the app also uses @vercel/flags-core directly, create the client once and pass it to the adapter so both share the same instance:
import { createClient } from '@vercel/flags-core';
import { createVercelAdapter } from '@flags-sdk/vercel';
const vercelFlagsClient = createClient(); // Vercel OIDC token, on deployments and after `vercel env pull`
const vercelAdapter = createVercelAdapter(vercelFlagsClient);
export const exampleFlag = flag({
key: 'example-flag',
adapter: vercelAdapter,
});Outside Vercel, pass the SDK key: createClient(process.env.FLAGS). Unlike vercelAdapter(), createClient() does not read FLAGS on its own.
Core client in other frameworks (for example, Express)
For frameworks without a Flags SDK entrypoint, use @vercel/flags-core directly. Create a shared client at module scope, but call evaluate() or bulkEvaluate() inside a request handler. Both initialize the client automatically; do not add a module-scope client.initialize() call or cache its promise for handlers to await.
// src/flags.ts
import { createClient } from '@vercel/flags-core';
const client = createClient();
// Call from a request handler, not during module loading.
export async function getVersion(): Promise<number> {
const result = await client.evaluate<number>('version', 0);
return result.value;
}With Vercel OIDC, the token can come from request context and may not exist during module loading. Even embedded definitions require OIDC to select the entry by the token's project_id. Local .env.local credentials can hide this timing problem. Explicit initialization is optional and must wait until authentication is available; awaiting an already-started initialization promise later does not move it into request context.
vercel flags CLI
Manage Vercel Flags from the terminal with an authenticated CLI and a targeted project (Project targeting). SDK installation and vercel env pull are app-development steps, not CLI prerequisites (see Setup).
For the current subcommand list and options, run vercel flags --help or vercel flags <cmd> --help. For CLI-wide contracts (linking, --non-interactive, --yes, parsing stdout) follow the vercel-cli skill. This section covers only what --help cannot tell you.
How the CLI connects to the SDK
- Key: the flag slug you pass to the CLI is the
keyinflag(). The flag returns one of the variants you created on Vercel (Run an A/B test). - Kind → type:
inspect <key>prints the kind and variants.boolean→flag<boolean>(),string→flag<string>(),number→flag<number>(),json→flag<YourType>(). The kind is fixed at creation and cannot be changed (Flag types). - Variants →
options:optionson the declaration are optional. They give Flags Explorer a dropdown, pre-fill the dashboard when a draft is promoted, and let precompute serialize values andgeneratePermutationsenumerate them (Declaring options). When you declare them, keep them equal to the variantsinspectshows. defaultValue: a flag that is archived, or declared in code but not created on Vercel (a draft), evaluates todefaultValue. WithoutdefaultValue, evaluation throws (Archive, Drafts).- Targeting attributes:
split,rollout, andrules addtake--by <entity>.<attribute>(and--condition <entity>.<attribute>:<op>:<value>). Define the entity and attribute under Flags → Entities first, and return the same names fromidentify(); the docs use aUserentity with anidattribute and--by user.id(Roll out a feature, Entities). When the attribute is missing from the context, a split or rollout serves its fallback variant (--default-variantin the CLI) and a rule that references it is skipped. Verify withevaluations. - Timestamp conditions: for Timestamp attributes,
--conditionacceptsgt,lt,gte,lteor the aliasesafter,before,at-or-after,at-or-beforewith an ISO 8601 date-time or epoch milliseconds, for example--condition system.time:at-or-after:2026-04-16T09:00:00Z.afterandbeforeare exclusive; other operators are rejected. A date-time without a timezone uses the machine's local timezone, so includeZor an offset.--byrejects Timestamp attributes. See Attribute types. - Authentication: on Vercel,
vercelAdapter()authenticates with the project's OIDC token and uses the configuration of the current environment;vercel env pullbrings that credential to.env.localfor local development. SDK keys (FLAGS) are for manual authentication: apps outside Vercel, custom environments, or flags owned by another project. Each SDK key is scoped to one environment and its full value is shown once, at creation (Getting started, SDK Keys). - Overrides: Flags Explorer stores overrides in a cookie signed with
FLAGS_SECRET; flags declared with the SDK honour it automatically (Handling overrides).vercel flags override <key>=<value>produces the same token for thevercel-flag-overridescookie and readsFLAGS_SECRETfrom the environment or.env.local, so use the secret of the environment you test against (see FLAGS_SECRET). - Embedded definitions: Vercel builds fetch the flag definitions once and bundle them into the deployment when the project uses
@flags-sdk/vercelor@vercel/flags-coreand the build can authenticate. This keeps every function on one snapshot and serves as the runtime fallback when the service is unreachable; opt out withVERCEL_FLAGS_DISABLE_DEFINITION_EMBEDDING=1(Embedded definitions).vercel flags prepareis the same step for builds that run outside Vercel.
Lifecycle and safety
The docs describe these flows end to end: Roll out a feature, Run an A/B test. For cleanup, follow the deployment and evaluation checks below before archiving.
- Promote: deploy the code to preview,
enableorsetthe flag in preview, verify on the preview URL, deploy to production, then change production (enable,set,split, orrollout). Each environment keeps its own configuration; preview stays on its current value until you change it. - Serve vs define:
enable/disablework on boolean flags only.setchanges the served variant for any kind.updateadds, removes, or renames variants and does not change what is served. A variant can only be removed when no environment configuration or rule references it, including rules that are stored but not active (Deleting a variant). - Static value vs targeting:
set/enable/disableput the environment in static value mode; its split, rollout, and rules are preserved in the background.use-targetingswitches back to targets and rules mode (Switching between static and rules modes). Runinspectfirst so you know what the environment serves today. - Confirm a change:
inspectfor the served state,versionsfor the change history (the dashboard can restore any earlier configuration),evaluationsto confirm traffic reaches the new variant or to check whether a flag is still evaluated before archiving (Evaluation metrics). Local development evaluates the Development environment configuration. - Deploy removal first: search for the flag key and its camelCase name, remove the declaration and conditionals, verify in preview, and complete the rollout to production and every other environment using the flag. A merged PR or preview deployment is not sufficient. Older deployments can still evaluate it through Skew Protection: inspect the project's configured maximum age and account for longer-lived traffic, including crawler exceptions, rather than assuming a fixed 24 or 48 hours.
- Verify no evaluations: before archiving, use
vercel flags evaluations <flag> --since <window> --jsonwith the explicit project/team target. Require no evaluations over an observation window after rollout that covers the applicable Skew Protection period. Check that the returned time range covers the window and the data is not truncated. Clients using@vercel/flags-corebefore 1.6.0 do not report evaluations, so verify reporting coverage; an empty result alone does not prove disuse (Evaluation metrics). If evaluations continue, reporting coverage is unknown, or the window has not elapsed, leave the flag active. - Archive before delete: only after those checks pass,
archivethe flag.unarchiverestores its configuration and history. Keep it archived through the agreed observation period before permanent deletion;rmrequires an archived flag and cannot be undone (Archive). - Agent runs:
archive,unarchive,rm, andupdate --remove-variantprompt for confirmation. Pass--yeswhen the user has approved the action.
CLI reference: https://vercel.com/docs/cli/flags
Global Config
Package: @flags-sdk/global-config
pnpm i @flags-sdk/global-configEnv: GLOBAL_CONFIG="global-config-connection-string"
Usage
import { flag } from 'flags/next';
import { globalConfigAdapter } from '@flags-sdk/global-config';
export const exampleFlag = flag({
adapter: globalConfigAdapter,
key: 'example-flag',
});Global Config should contain:
{
"flags": {
"example-flag": true,
"another-flag": false
}
}Custom configuration
import { createGlobalConfigAdapter } from '@flags-sdk/global-config';
const myAdapter = createGlobalConfigAdapter({
connectionString: process.env.OTHER_GLOBAL_CONFIG,
options: {
globalConfigItemKey: 'other-flags-key',
teamSlug: 'my-team',
},
});Statsig
Package: @flags-sdk/statsig
pnpm i @flags-sdk/statsigEnv vars:
STATSIG_SERVER_API_KEY(required)STATSIG_PROJECT_ID(optional)EXPERIMENTATION_CONFIG(optional, Global Config)EXPERIMENTATION_CONFIG_ITEM_KEY(optional)
Methods
import { statsigAdapter, type StatsigUser } from '@flags-sdk/statsig';
// Feature Gates
export const myGate = flag<boolean, StatsigUser>({
key: 'my_feature_gate',
adapter: statsigAdapter.featureGate((gate) => gate.value),
identify,
});
// Dynamic Configs
export const myConfig = flag<Record<string, unknown>, StatsigUser>({
key: 'my_dynamic_config',
adapter: statsigAdapter.dynamicConfig((config) => config.value),
identify,
});
// Experiments
export const myExperiment = flag<Record<string, unknown>, StatsigUser>({
key: 'my_experiment',
adapter: statsigAdapter.experiment((config) => config.value),
identify,
});
// Autotune
export const myAutotune = flag<Record<string, unknown>, StatsigUser>({
key: 'my_autotune',
adapter: statsigAdapter.autotune((config) => config.value),
identify,
});
// Layers
export const myLayer = flag<Record<string, unknown>, StatsigUser>({
key: 'my_layer',
adapter: statsigAdapter.layer((layer) => layer.value),
identify,
});Same key, different mappings
Use . to distinguish flags from the same config:
export const text = flag<string, StatsigUser>({
key: 'my_config.text',
adapter: statsigAdapter.dynamicConfig((c) => c.value.text as string),
identify,
});
export const price = flag<number, StatsigUser>({
key: 'my_config.price',
adapter: statsigAdapter.dynamicConfig((c) => c.value.price as number),
identify,
});Exposure logging
Disabled by default (middleware prefetch would cause premature exposures). Enable explicitly:
adapter: statsigAdapter.featureGate((gate) => gate.value, {
exposureLogging: true,
})Log exposures from the client instead when possible.
Flags Explorer
import { getProviderData as getStatsigProviderData } from '@flags-sdk/statsig';
import { mergeProviderData } from 'flags';
export const GET = createFlagsDiscoveryEndpoint(async () => {
return mergeProviderData([
getProviderData(flags),
getStatsigProviderData({
consoleApiKey: process.env.STATSIG_CONSOLE_API_KEY,
projectId: process.env.STATSIG_PROJECT_ID,
}),
]);
});LaunchDarkly
Package: @flags-sdk/launchdarkly
pnpm i @flags-sdk/launchdarklyEnv vars:
LAUNCHDARKLY_CLIENT_SIDE_ID(required)LAUNCHDARKLY_PROJECT_SLUG(required)GLOBAL_CONFIG(required)
Usage
import { ldAdapter, type LDContext } from '@flags-sdk/launchdarkly';
const identify = dedupe((async ({ headers, cookies }) => {
const user = await getUser(headers, cookies);
return { key: user.userID };
}) satisfies Identify<LDContext>);
export const exampleFlag = flag<boolean, LDContext>({
key: 'example-flag',
identify,
adapter: ldAdapter.variation(),
});Flags Explorer
import { getProviderData as getLDProviderData } from '@flags-sdk/launchdarkly';
return mergeProviderData([
getProviderData(flags),
getLDProviderData({
apiKey: process.env.LAUNCHDARKLY_API_KEY,
projectKey: process.env.LAUNCHDARKLY_PROJECT_KEY,
environment: process.env.LAUNCHDARKLY_ENVIRONMENT,
}),
]);PostHog
Package: @flags-sdk/posthog
pnpm i @flags-sdk/posthogEnv vars, always required:
POSTHOG_HOST(e.g.https://us.i.posthog.comorhttps://eu.i.posthog.com)POSTHOG_PROJECT_API_KEY(phc_...)
Optional, opts into local evaluation (background polling) instead of remote:
POSTHOG_SECRET_KEY(phs_...)
For the Flags Explorer (getProviderData only):
POSTHOG_PERSONAL_API_KEY(phx_...)POSTHOG_PROJECT_ID(e.g.521742)
Methods
import { postHogAdapter } from '@flags-sdk/posthog';
// Value — boolean flag. Pass the adapter uninvoked or invoked, both work.
export const myFlag = flag<boolean>({
key: 'my-flag',
adapter: postHogAdapter,
identify,
});
// Value — multivariate flag resolves to the variant string
export const myVariant = flag<string>({
key: 'my-flag',
adapter: postHogAdapter,
identify,
});
// Payload
export const myPayload = flag({
key: 'my-flag',
adapter: postHogAdapter.payload,
defaultValue: {},
identify,
});identify must return { distinctId }.
Flags Explorer
Requires: POSTHOG_PERSONAL_API_KEY, POSTHOG_PROJECT_ID
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!,
}),
);GrowthBook
Package: @flags-sdk/growthbook
pnpm i @flags-sdk/growthbookEnv: GROWTHBOOK_CLIENT_KEY (required)
Usage
import { growthbookAdapter, type Attributes } from '@flags-sdk/growthbook';
const identify = dedupe((async ({ cookies }) => ({
id: cookies.get('user_id')?.value,
})) satisfies Identify<Attributes>);
export const myFlag = flag({
key: 'my_feature',
identify,
adapter: growthbookAdapter.feature<boolean>(),
});Global Config
Set GROWTHBOOK_EDGE_CONNECTION_STRING or EXPERIMENTATION_CONFIG (Vercel Marketplace).
Tracking
growthbookAdapter.setTrackingCallback((experiment, result) => {
after(async () => {
console.log('Experiment', experiment.key, 'Variation', result.key);
});
});Flagsmith
Package: @flags-sdk/flagsmith
pnpm i @flags-sdk/flagsmithEnv: FLAGSMITH_ENVIRONMENT_KEY (required)
Remote evaluation is the default for serverless deployments. For a long-running server, use createFlagsmithAdapter({ environmentKey, enableLocalEvaluation: true }) with a server-side key to opt into local evaluation and environment polling. Call adapter.close() on shutdown to stop polling.
Usage with type coercion
import { flagsmithAdapter } from '@flags-sdk/flagsmith';
export const buttonColor = flag<string>({
key: 'button-color',
defaultValue: 'blue',
adapter: flagsmithAdapter.getValue({ coerce: 'string' }),
});
export const showBanner = flag<boolean>({
key: 'show-banner',
defaultValue: false,
adapter: flagsmithAdapter.getValue({ coerce: 'boolean' }),
});Coercion options: 'string', 'number', 'boolean', or omit for raw value.
Reflag
Package: @flags-sdk/reflag
pnpm i @flags-sdk/reflagEnv: REFLAG_SECRET_KEY
import { reflagAdapter, type Context } from '@flags-sdk/reflag';
const identify = dedupe((async ({ headers, cookies }) => ({
user: { id: 'user-id', name: 'name', email: 'email' },
company: { id: 'company-id' },
})) satisfies Identify<Context>);
export const myFeature = flag<boolean, Context>({
key: 'my_feature',
identify,
adapter: reflagAdapter.isEnabled(),
});OpenFeature
Package: @flags-sdk/openfeature + @openfeature/server-sdk
pnpm i @flags-sdk/openfeature @openfeature/server-sdkSetup
import { createOpenFeatureAdapter } from '@flags-sdk/openfeature';
// Sync provider
OpenFeature.setProvider(new YourProvider());
const adapter = createOpenFeatureAdapter(OpenFeature.getClient());
// Async provider
const adapter = createOpenFeatureAdapter(async () => {
await OpenFeature.setProviderAndWait(new YourProvider());
return OpenFeature.getClient();
});Methods
adapter.booleanValue() // boolean flags
adapter.stringValue() // string flags
adapter.numberValue() // number flags
adapter.objectValue() // object flagsAll require defaultValue on the flag declaration.
Split
Package: @flags-sdk/split (Flags Explorer only, adapter coming soon)
import { getProviderData as getSplitProviderData } from '@flags-sdk/split';
getSplitProviderData({
adminApiKey: process.env.SPLIT_ADMIN_API_KEY,
environmentId: process.env.SPLIT_ENVIRONMENT_ID,
organizationId: process.env.SPLIT_ORG_ID,
workspaceId: process.env.SPLIT_WORKSPACE_ID,
});Optimizely
Package: @flags-sdk/optimizely (Flags Explorer only, adapter coming soon)
import { getProviderData as getOptimizelyProviderData } from '@flags-sdk/optimizely';
getOptimizelyProviderData({
projectId: process.env.OPTIMIZELY_PROJECT_ID,
apiKey: process.env.OPTIMIZELY_API_KEY,
});Custom Adapters
Create an adapter factory:
import type { Adapter } from 'flags';
export function createMyAdapter(/* options */) {
return function myAdapter<ValueType, EntitiesType>(): Adapter<ValueType, EntitiesType> {
return {
origin(key) {
return `https://my-provider.com/flags/${key}`;
},
async decide({ key }): Promise<ValueType> {
return false as ValueType;
},
};
};
}Bulk evaluation (bulkDecide)
Adapters can implement an optional bulkDecide hook. When set (and the adapter has an adapterId), evaluate() calls it once for every group of flags that share this adapter and the same identify source — instead of calling decide per flag. This lets the provider share work across evaluations (e.g. a single network request for many flags).
return {
adapterId: 'my-provider', // required for bulkDecide to be used
origin(key) {
return `https://my-provider.com/flags/${key}`;
},
async decide({ key }): Promise<ValueType> {
return false as ValueType;
},
// Called by evaluate() for a batch of flags sharing this adapter + identify
async bulkDecide({ flags, entities, headers, cookies }) {
// flags: { key: string; defaultValue?: unknown }[]
// Return a record keyed by flag key.
return Object.fromEntries(
flags.map(({ key }) => [key, false as ValueType]),
);
},
};Contract:
- Return
Record<flagKey, value>. Missing keys orvalue: undefinedfall back to each flag'sdefaultValue. - Throwing falls back to
defaultValueper flag (and rejects for flags without adefaultValue). - A flag with an inline
decidetakes precedence and is excluded from bulk evaluation.
Default adapter pattern
Expose a lazily-initialized default for simpler usage:
let defaultAdapter: ReturnType<typeof createMyAdapter> | undefined;
export function myAdapter<V, E>(): Adapter<V, E> {
if (!defaultAdapter) {
if (!process.env.MY_API_KEY) throw new Error('Missing MY_API_KEY');
defaultAdapter = createMyAdapter(process.env.MY_API_KEY);
}
return defaultAdapter<V, E>();
}Usage:
import { myAdapter } from './my-adapter';
export const exampleFlag = flag({
key: 'example',
adapter: myAdapter,
});