Feature flags in PostHog enable you to control feature rollouts, run A/B tests, and manage remote configuration without deploying new code. The Node.js SDK supports both local and remote evaluation of feature flags with extensive configuration options.
Check if a feature flag is enabled for a specific user.
/**
* Check if a feature flag is enabled for a specific user
* @param key - The feature flag key
* @param distinctId - The user's distinct ID
* @param options - Optional configuration for flag evaluation
* @returns Promise that resolves to true if enabled, false if disabled, undefined if not found
*/
async isFeatureEnabled(
key: string,
distinctId: string,
options?: {
groups?: Record<string, string>
personProperties?: Record<string, string>
groupProperties?: Record<string, Record<string, string>>
onlyEvaluateLocally?: boolean
sendFeatureFlagEvents?: boolean
disableGeoip?: boolean
}
): Promise<boolean | undefined>Usage Examples:
import { PostHog } from 'posthog-node'
const client = new PostHog('your-api-key', {
host: 'https://app.posthog.com'
})
// Basic feature flag check
const isEnabled = await client.isFeatureEnabled('new-feature', 'user_123')
if (isEnabled) {
// Feature is enabled
console.log('New feature is active')
} else {
// Feature is disabled
console.log('New feature is not active')
}
// With groups and properties
const isEnabled = await client.isFeatureEnabled('org-feature', 'user_123', {
groups: { organization: 'acme-corp' },
personProperties: { plan: 'enterprise' }
})
// Only evaluate locally (no server call if local evaluation not ready)
const isEnabled = await client.isFeatureEnabled('quick-check', 'user_123', {
onlyEvaluateLocally: true
})Get the value of a feature flag for multivariate flags or string-based flags.
/**
* Get the value of a feature flag for a specific user
* @param key - The feature flag key
* @param distinctId - The user's distinct ID
* @param options - Optional configuration for flag evaluation
* @returns Promise that resolves to the flag value (boolean | string) or undefined
*/
async getFeatureFlag(
key: string,
distinctId: string,
options?: {
groups?: Record<string, string>
personProperties?: Record<string, string>
groupProperties?: Record<string, Record<string, string>>
onlyEvaluateLocally?: boolean
sendFeatureFlagEvents?: boolean
disableGeoip?: boolean
}
): Promise<FeatureFlagValue | undefined>
type FeatureFlagValue = boolean | stringUsage Examples:
// Basic feature flag check
const flagValue = await client.getFeatureFlag('new-feature', 'user_123')
if (flagValue === 'variant-a') {
// Show variant A
} else if (flagValue === 'variant-b') {
// Show variant B
} else {
// Flag is disabled or not found
}
// With groups and properties for targeted rollout
const flagValue = await client.getFeatureFlag('org-feature', 'user_123', {
groups: { organization: 'acme-corp' },
personProperties: { plan: 'enterprise' },
groupProperties: { organization: { tier: 'premium' } }
})
// Only evaluate locally
const flagValue = await client.getFeatureFlag('local-flag', 'user_123', {
onlyEvaluateLocally: true
})
// Disable feature flag events
const flagValue = await client.getFeatureFlag('silent-flag', 'user_123', {
sendFeatureFlagEvents: false // Don't send $feature_flag_called event
})Get the JSON payload attached to a feature flag.
/**
* Get the payload for a feature flag
* @param key - The feature flag key
* @param distinctId - The user's distinct ID
* @param matchValue - Optional match value to get payload for specific variant
* @param options - Optional configuration for flag evaluation
* @returns Promise that resolves to the flag payload or undefined
*/
async getFeatureFlagPayload(
key: string,
distinctId: string,
matchValue?: FeatureFlagValue,
options?: {
groups?: Record<string, string>
personProperties?: Record<string, string>
groupProperties?: Record<string, Record<string, string>>
onlyEvaluateLocally?: boolean
sendFeatureFlagEvents?: boolean
disableGeoip?: boolean
}
): Promise<JsonType | undefined>
type JsonType = string | number | boolean | null | JsonType[] | { [key: string]: JsonType }Usage Examples:
// Get payload for a feature flag
const payload = await client.getFeatureFlagPayload('flag-key', 'user_123')
if (payload) {
console.log('Flag payload:', payload)
// payload can be any JSON structure: string, number, boolean, object, array
}
// Get payload with specific match value
const payload = await client.getFeatureFlagPayload('flag-key', 'user_123', 'variant-a')
// This gets the payload specifically for variant-a
// With groups and properties
const payload = await client.getFeatureFlagPayload('org-flag', 'user_123', undefined, {
groups: { organization: 'acme-corp' },
personProperties: { plan: 'enterprise' }
})
// Example: Using payload for configuration
const payload = await client.getFeatureFlagPayload('button-config', 'user_123')
if (payload && typeof payload === 'object') {
const config = payload as { color: string; text: string; size: string }
console.log(`Button color: ${config.color}`)
}Get remote configuration payload for encrypted feature flags.
/**
* Get the remote config payload for a feature flag
* @param flagKey - The feature flag key
* @returns Promise that resolves to the remote config payload or undefined
* @throws Error if personal API key is not provided
*/
async getRemoteConfigPayload(flagKey: string): Promise<JsonType | undefined>Usage Examples:
// Initialize client with personal API key (required)
const client = new PostHog('your-api-key', {
host: 'https://app.posthog.com',
personalApiKey: 'phx_...' // Required for remote config
})
// Get remote config payload
const payload = await client.getRemoteConfigPayload('flag-key')
if (payload) {
console.log('Remote config payload:', payload)
}
// Example: Using remote config for sensitive settings
const dbConfig = await client.getRemoteConfigPayload('database-config')
if (dbConfig && typeof dbConfig === 'object') {
const config = dbConfig as { host: string; port: number; name: string }
// Use encrypted config values
connectToDatabase(config)
}Get all feature flag values for a specific user.
/**
* Get all feature flag values for a specific user
* @param distinctId - The user's distinct ID
* @param options - Optional configuration for flag evaluation
* @returns Promise that resolves to a record of flag keys and their values
*/
async getAllFlags(
distinctId: string,
options?: {
groups?: Record<string, string>
personProperties?: Record<string, string>
groupProperties?: Record<string, Record<string, string>>
onlyEvaluateLocally?: boolean
disableGeoip?: boolean
flagKeys?: string[]
}
): Promise<Record<string, FeatureFlagValue>>Usage Examples:
// Get all flags for a user
const allFlags = await client.getAllFlags('user_123')
console.log('User flags:', allFlags)
// Output: { 'flag-1': 'variant-a', 'flag-2': false, 'flag-3': 'variant-b' }
// Check multiple flags efficiently
if (allFlags['new-ui']) {
// Show new UI
}
if (allFlags['experimental-feature'] === 'test') {
// Enable test mode
}
// With specific flag keys (only evaluate these flags)
const specificFlags = await client.getAllFlags('user_123', {
flagKeys: ['flag-1', 'flag-2', 'flag-3']
})
// With groups and properties
const orgFlags = await client.getAllFlags('user_123', {
groups: { organization: 'acme-corp' },
personProperties: { plan: 'enterprise' },
groupProperties: { organization: { employees: 500 } }
})
// Only use local evaluation
const localFlags = await client.getAllFlags('user_123', {
onlyEvaluateLocally: true
})Get all feature flag values and their payloads for a specific user.
/**
* Get all feature flag values and payloads for a specific user
* @param distinctId - The user's distinct ID
* @param options - Optional configuration for flag evaluation
* @returns Promise that resolves to flags and payloads
*/
async getAllFlagsAndPayloads(
distinctId: string,
options?: {
groups?: Record<string, string>
personProperties?: Record<string, string>
groupProperties?: Record<string, Record<string, string>>
onlyEvaluateLocally?: boolean
disableGeoip?: boolean
flagKeys?: string[]
}
): Promise<PostHogFlagsAndPayloadsResponse>
interface PostHogFlagsAndPayloadsResponse {
featureFlags: Record<string, FeatureFlagValue>
featureFlagPayloads: Record<string, JsonType>
}Usage Examples:
// Get all flags and payloads for a user
const result = await client.getAllFlagsAndPayloads('user_123')
console.log('Flags:', result.featureFlags)
console.log('Payloads:', result.featureFlagPayloads)
// Use flags and payloads together
if (result.featureFlags['new-button']) {
const config = result.featureFlagPayloads['new-button']
if (config && typeof config === 'object') {
renderButton(config as { color: string; text: string })
}
}
// With specific flag keys
const result = await client.getAllFlagsAndPayloads('user_123', {
flagKeys: ['flag-1', 'flag-2']
})
// Only evaluate locally
const result = await client.getAllFlagsAndPayloads('user_123', {
onlyEvaluateLocally: true
})
// With groups for organization-level flags
const result = await client.getAllFlagsAndPayloads('user_123', {
groups: { organization: 'acme-corp', team: 'engineering' }
})Check if local evaluation is ready for feature flags.
/**
* Check if local evaluation of feature flags is ready
* @returns true if local evaluation is ready, false otherwise
*/
isLocalEvaluationReady(): booleanUsage Examples:
// Check if ready
if (client.isLocalEvaluationReady()) {
// Local evaluation is ready, can evaluate flags locally
const flag = await client.getFeatureFlag('flag-key', 'user_123')
} else {
// Local evaluation not ready, will use remote evaluation
const flag = await client.getFeatureFlag('flag-key', 'user_123')
}
// Use in conditional logic
const evaluationMode = client.isLocalEvaluationReady() ? 'local' : 'remote'
console.log(`Using ${evaluationMode} evaluation`)Wait for local evaluation to be ready with optional timeout.
/**
* Wait for local evaluation of feature flags to be ready
* @param timeoutMs - Timeout in milliseconds (default: 30000)
* @returns Promise that resolves to true if ready, false if timed out
*/
async waitForLocalEvaluationReady(timeoutMs?: number): Promise<boolean>Usage Examples:
// Wait for local evaluation
const isReady = await client.waitForLocalEvaluationReady()
if (isReady) {
console.log('Local evaluation is ready')
// Now safe to use flags with onlyEvaluateLocally: true
} else {
console.log('Local evaluation timed out')
}
// Wait with custom timeout
const isReady = await client.waitForLocalEvaluationReady(10000) // 10 seconds
if (isReady) {
// Proceed with local evaluation
}
// Use at application startup
async function initializeApp() {
const client = new PostHog('your-api-key', {
host: 'https://app.posthog.com',
personalApiKey: 'phx_...'
})
// Wait for flags to be ready before serving requests
await client.waitForLocalEvaluationReady(5000)
// Start server
app.listen(3000)
}Force reload feature flag definitions from the server.
/**
* Reload feature flag definitions from the server for local evaluation
* @returns Promise that resolves when flags are reloaded
*/
async reloadFeatureFlags(): Promise<void>Usage Examples:
// Force reload of feature flags
await client.reloadFeatureFlags()
console.log('Feature flags reloaded')
// Reload before checking a specific flag
await client.reloadFeatureFlags()
const flag = await client.getFeatureFlag('flag-key', 'user_123')
// Use in a scheduled job
setInterval(async () => {
await client.reloadFeatureFlags()
console.log('Flags refreshed')
}, 60000) // Refresh every minute
// Reload on demand (e.g., webhook endpoint)
app.post('/webhooks/flags-updated', async (req, res) => {
await client.reloadFeatureFlags()
res.json({ success: true })
})PostHog feature flags can be evaluated in two ways: locally or remotely. Understanding the difference is crucial for optimal performance.
Local evaluation evaluates feature flags within your application using cached flag definitions. This is faster and reduces API calls.
Requirements:
Benefits:
Setup:
const client = new PostHog('your-api-key', {
host: 'https://app.posthog.com',
personalApiKey: 'phx_...', // Required for local evaluation
enableLocalEvaluation: true, // Default: true
featureFlagsPollingInterval: 30000 // Default: 30 seconds (minimum: 100ms)
})
// Wait for flags to load
await client.waitForLocalEvaluationReady()
// Now flags are evaluated locally
const flag = await client.getFeatureFlag('flag-key', 'user_123')Example:
// Initialize with local evaluation
const client = new PostHog('your-api-key', {
host: 'https://app.posthog.com',
personalApiKey: 'phx_...',
featureFlagsPollingInterval: 60000 // Poll every 60 seconds
})
// Check readiness
console.log('Local eval ready:', client.isLocalEvaluationReady())
// Wait for ready state
const ready = await client.waitForLocalEvaluationReady(5000)
if (ready) {
// Fast local evaluation
const flag = await client.getFeatureFlag('my-flag', 'user_123')
}Remote evaluation makes an API call to PostHog servers for each flag check. This ensures you always have the latest flag values but is slower.
Use When:
Setup:
// Without personal API key, uses remote evaluation
const client = new PostHog('your-api-key', {
host: 'https://app.posthog.com'
})
// Each call makes an API request
const flag = await client.getFeatureFlag('flag-key', 'user_123')Example:
// Remote evaluation with user properties
const flag = await client.getFeatureFlag('premium-feature', 'user_123', {
personProperties: {
email: 'user@example.com',
plan: 'premium'
}
})
// Remote evaluation with GeoIP
const flag = await client.getFeatureFlag('region-feature', 'user_123', {
disableGeoip: false // Allow GeoIP-based targeting
})You can combine both approaches for optimal performance:
const client = new PostHog('your-api-key', {
host: 'https://app.posthog.com',
personalApiKey: 'phx_...'
})
// Try local first, fallback to remote if needed
async function getFlag(key: string, userId: string) {
if (client.isLocalEvaluationReady()) {
// Use local evaluation
return await client.getFeatureFlag(key, userId, {
onlyEvaluateLocally: false // Allow fallback to remote
})
} else {
// Use remote evaluation
return await client.getFeatureFlag(key, userId)
}
}All feature flag methods support a rich set of options for advanced use cases.
Associate the flag check with groups (e.g., organizations, teams).
const flag = await client.getFeatureFlag('org-feature', 'user_123', {
groups: {
organization: 'acme-corp',
team: 'engineering'
}
})Use Cases:
Provide user properties for local evaluation without making additional API calls.
const flag = await client.getFeatureFlag('premium-feature', 'user_123', {
personProperties: {
plan: 'enterprise',
role: 'admin',
email: 'user@example.com'
}
})Use Cases:
Provide group properties for local evaluation.
const flag = await client.getFeatureFlag('org-feature', 'user_123', {
groups: { organization: 'acme-corp' },
groupProperties: {
organization: {
tier: 'enterprise',
employees: 500,
region: 'us-west'
}
}
})Use Cases:
Force local evaluation only, never fall back to remote.
const flag = await client.getFeatureFlag('flag-key', 'user_123', {
onlyEvaluateLocally: true
})
// If local evaluation not ready or flag requires server evaluation,
// returns undefined instead of making API callUse Cases:
Control whether $feature_flag_called events are sent.
const flag = await client.getFeatureFlag('flag-key', 'user_123', {
sendFeatureFlagEvents: false // Don't send events
})Use Cases:
Disable GeoIP-based targeting for the flag check.
const flag = await client.getFeatureFlag('flag-key', 'user_123', {
disableGeoip: true
})Use Cases:
Specify which flags to evaluate (for getAllFlags and getAllFlagsAndPayloads).
const flags = await client.getAllFlags('user_123', {
flagKeys: ['flag-1', 'flag-2', 'flag-3']
})
// Only returns values for specified flagsUse Cases:
Run A/B tests with multivariate flags and payloads.
// Define flag with variants in PostHog UI:
// - variant-a (50%)
// - variant-b (50%)
async function renderPage(userId: string) {
const variant = await client.getFeatureFlag('homepage-test', userId)
switch (variant) {
case 'variant-a':
return renderVariantA()
case 'variant-b':
return renderVariantB()
default:
return renderDefault()
}
}
// Track conversion
async function trackPurchase(userId: string, amount: number) {
client.capture({
distinctId: userId,
event: 'purchase',
properties: { amount }
})
}Gradually roll out features to users.
async function showNewFeature(userId: string) {
const isEnabled = await client.isFeatureEnabled('new-feature', userId)
if (isEnabled) {
return renderNewFeature()
} else {
return renderOldFeature()
}
}
// In PostHog UI, gradually increase rollout percentage:
// 5% -> 10% -> 25% -> 50% -> 100%Run experiments with payload-based configuration.
// Configure in PostHog UI with payloads:
// variant-a: { color: "blue", cta: "Buy Now" }
// variant-b: { color: "green", cta: "Get Started" }
async function renderCTA(userId: string) {
const payload = await client.getFeatureFlagPayload('cta-experiment', userId)
if (payload && typeof payload === 'object') {
const config = payload as { color: string; cta: string }
return `<button style="background: ${config.color}">${config.cta}</button>`
}
// Default
return '<button>Learn More</button>'
}Use feature flags as remote configuration.
// Configure in PostHog UI with payload:
// { apiUrl: "https://api.example.com", timeout: 5000, retries: 3 }
async function getApiConfig() {
const config = await client.getRemoteConfigPayload('api-config')
if (config && typeof config === 'object') {
return config as {
apiUrl: string
timeout: number
retries: number
}
}
// Default config
return {
apiUrl: 'https://api.example.com',
timeout: 3000,
retries: 1
}
}
// Use config
const config = await getApiConfig()
const response = await fetch(config.apiUrl, {
timeout: config.timeout
})Enable features based on user properties and groups.
async function hasAccessToFeature(userId: string, userPlan: string, orgId: string) {
const hasAccess = await client.isFeatureEnabled('premium-feature', userId, {
personProperties: { plan: userPlan },
groups: { organization: orgId }
})
return hasAccess
}
// Usage
const canAccess = await hasAccessToFeature('user_123', 'enterprise', 'org_456')
if (canAccess) {
showPremiumFeature()
}Load all flags at application startup.
async function bootstrapApp() {
const client = new PostHog('your-api-key', {
host: 'https://app.posthog.com',
personalApiKey: 'phx_...'
})
// Wait for flags to load
const ready = await client.waitForLocalEvaluationReady(5000)
if (!ready) {
console.warn('Flags not ready, using defaults')
}
// Get all flags for current user
const flags = await client.getAllFlags('current-user')
// Store in app state
app.state.featureFlags = flags
// Start app
app.listen(3000)
}Send feature flag values with events automatically.
// Capture event with feature flags
client.capture({
distinctId: 'user_123',
event: 'button_clicked',
properties: { button_id: 'cta' },
sendFeatureFlags: true // Include all active flags
})
// With specific flags
client.capture({
distinctId: 'user_123',
event: 'page_viewed',
properties: { page: '/pricing' },
sendFeatureFlags: {
flagKeys: ['pricing-test', 'new-plans']
}
})
// Only use local evaluation for flags
client.capture({
distinctId: 'user_123',
event: 'checkout_started',
sendFeatureFlags: {
onlyEvaluateLocally: true
}
})Always provide a personal API key for better performance:
// Good
const client = new PostHog('your-api-key', {
host: 'https://app.posthog.com',
personalApiKey: 'phx_...' // Enable local evaluation
})
// Not optimal (remote evaluation only)
const client = new PostHog('your-api-key', {
host: 'https://app.posthog.com'
})Ensure flags are ready before serving requests:
async function startServer() {
const client = new PostHog('your-api-key', {
host: 'https://app.posthog.com',
personalApiKey: 'phx_...'
})
// Wait for flags (with timeout)
await client.waitForLocalEvaluationReady(5000)
app.listen(3000)
console.log('Server started with feature flags loaded')
}For frequently checked flags, cache the value per request or session:
class FeatureFlagCache {
private cache: Map<string, { value: any; timestamp: number }> = new Map()
private ttl = 60000 // 1 minute
async getFlag(key: string, userId: string): Promise<any> {
const cacheKey = `${key}:${userId}`
const cached = this.cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.value
}
const value = await client.getFeatureFlag(key, userId)
this.cache.set(cacheKey, { value, timestamp: Date.now() })
return value
}
}Pass person and group properties for accurate local evaluation:
// Good - properties included
const flag = await client.getFeatureFlag('feature', 'user_123', {
personProperties: { plan: 'premium', email: 'user@example.com' },
groups: { organization: 'acme' },
groupProperties: { organization: { tier: 'enterprise' } }
})
// Not optimal - may require server evaluation
const flag = await client.getFeatureFlag('feature', 'user_123')For B2B applications, use groups for organization-level flags:
// Associate user with organization
client.capture({
distinctId: 'user_123',
event: 'page_view',
groups: { organization: 'acme-corp' }
})
// Check organization flag
const hasFeature = await client.isFeatureEnabled('org-feature', 'user_123', {
groups: { organization: 'acme-corp' }
})Always handle the case where a flag is undefined:
// Good
const flag = await client.getFeatureFlag('new-feature', 'user_123')
if (flag === 'variant-a') {
renderVariantA()
} else if (flag === 'variant-b') {
renderVariantB()
} else {
// Flag disabled, not found, or error
renderDefault()
}
// Not safe
const flag = await client.getFeatureFlag('new-feature', 'user_123')
// flag might be undefined!For flags checked on every request, disable event tracking:
// Checked frequently - disable events
const flag = await client.getFeatureFlag('rate-limit-config', userId, {
sendFeatureFlagEvents: false
})
// Important experiment - keep events
const variant = await client.getFeatureFlag('pricing-test', userId, {
sendFeatureFlagEvents: true // Default
})If checking multiple flags, use getAllFlags for better performance:
// Good - single call
const flags = await client.getAllFlags('user_123')
if (flags['feature-a']) { /* ... */ }
if (flags['feature-b']) { /* ... */ }
if (flags['feature-c']) { /* ... */ }
// Not optimal - multiple calls
const featureA = await client.getFeatureFlag('feature-a', 'user_123')
const featureB = await client.getFeatureFlag('feature-b', 'user_123')
const featureC = await client.getFeatureFlag('feature-c', 'user_123')After updating flag definitions, force a reload:
// Webhook endpoint for flag updates
app.post('/webhooks/posthog/flags-updated', async (req, res) => {
await client.reloadFeatureFlags()
console.log('Feature flags reloaded')
res.json({ success: true })
})Track when local evaluation is not ready:
async function checkFlag(key: string, userId: string) {
if (!client.isLocalEvaluationReady()) {
console.warn('Local evaluation not ready, using remote evaluation')
}
return await client.getFeatureFlag(key, userId)
}
// Or use events
client._events.on('error', (error) => {
console.error('Feature flag error:', error)
})
client._events.on('localEvaluationFlagsLoaded', (count) => {
console.log(`Loaded ${count} feature flags for local evaluation`)
})Create test utilities for flag variants:
// Test utility
class FeatureFlagTestHelper {
private overrides: Map<string, any> = new Map()
setFlag(key: string, value: any) {
this.overrides.set(key, value)
}
async getFlag(key: string, userId: string) {
if (this.overrides.has(key)) {
return this.overrides.get(key)
}
return await client.getFeatureFlag(key, userId)
}
clearOverrides() {
this.overrides.clear()
}
}
// In tests
const helper = new FeatureFlagTestHelper()
helper.setFlag('new-feature', 'variant-a')
// Test variant-a behaviorDefine types for flag values and payloads:
// Define flag types
type FeatureFlags = {
'new-ui': boolean
'pricing-test': 'control' | 'variant-a' | 'variant-b'
'button-config': { color: string; text: string }
}
// Type-safe wrapper
async function getFlag<K extends keyof FeatureFlags>(
key: K,
userId: string
): Promise<FeatureFlags[K] | undefined> {
return await client.getFeatureFlag(key, userId) as FeatureFlags[K] | undefined
}
// Usage
const uiEnabled = await getFlag('new-ui', 'user_123')
// Type: boolean | undefined
const variant = await getFlag('pricing-test', 'user_123')
// Type: 'control' | 'variant-a' | 'variant-b' | undefined