Claude Skill

api-flags-posthog-flags

PostHog feature flags, rollouts, A/B testing. Use when implementing gradual rollouts, A/B tests, kill switches, remote configuration, beta features, or user targeting with PostHog.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-flags-posthog-flags_skills_api-flags-posthog-flags-3a51ef5.zip · 19 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-flags-posthog-flags/skills/api-flags-posthog-flags
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Feature Flags with PostHog

Quick Guide: Use PostHog feature flags for gradual rollouts, A/B testing, and remote configuration. Client-side: useFeatureFlagEnabled hook. Server-side: posthog-node with local evaluation. Always pair useFeatureFlagPayload with useFeatureFlagEnabled for experiments. Handle the undefined loading state on every flag check.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST always pair useFeatureFlagPayload with useFeatureFlagEnabled or useFeatureFlagVariantKey for experiments - payload hooks don't send exposure events)

(You MUST use the feature flags secure API key (phs_*) for server-side local evaluation - personal API keys are deprecated for this use)

(You MUST handle the undefined state when flags are loading - never assume a flag is immediately available)

(You MUST include flag owner and expiry date in flag metadata - flags without owners become orphaned debt)

(You MUST wrap flag usage in a single function when used in multiple places - prevents orphaned flag code on cleanup)

</critical_requirements>


Auto-detection: PostHog feature flags, useFeatureFlagEnabled, useFeatureFlagPayload, useFeatureFlagVariantKey, PostHogFeature, isFeatureEnabled, getFeatureFlag, gradual rollout, A/B test, experiment, multivariate flag

When to use:

  • Gradual rollouts (deploy to 10% users, then 50%, then 100%)
  • A/B testing with experiments (measure impact of changes)
  • Kill switches (instantly disable features without deploy)
  • Remote configuration (change behavior without code changes)
  • Beta features opt-in (let users try new features)
  • User targeting (show features to specific cohorts)

When NOT to use:

  • Simple on/off switches that never change (use environment variables)
  • Configuration that must be compile-time (use build flags)
  • Secrets or sensitive data (use secret management)
  • Features that should always be on (just ship the code)

Key patterns covered:

  • Client-side flag evaluation with React hooks
  • Server-side local evaluation for performance
  • Boolean vs multivariate flags
  • Gradual rollouts with percentage targeting
  • A/B testing and experiments
  • Payloads for remote configuration
  • Local development overrides
  • Flag cleanup and lifecycle management

Detailed Resources:




<red_flags>

RED FLAGS

High Priority Issues:

  • Using useFeatureFlagPayload alone for experiments (no exposure tracking)
  • Exposing Feature Flags Secure API key (phs_*) on client (security violation)
  • No loading state handling (causes UI flash)
  • Flags without owners or expiry dates (becomes permanent debt)

Medium Priority Issues:

  • Magic string flag keys instead of constants (typos, hard to grep)
  • Complex targeting rules on high-traffic flags (performance hit)
  • Local evaluation in serverless/edge without external cache (cold start issues)
  • Not using PostHog toolbar for local testing (harder debugging)

Common Mistakes:

  • Checking flag in multiple places instead of wrapper function
  • Not bootstrapping flags for SSR (content flash on hydration)
  • Running experiments without defined primary metric
  • Peeking at experiment results before completion
  • Rolling out to 100% without cleanup plan

Gotchas & Edge Cases:

  • PostHog uses deterministic hashing - same user always gets same variant
  • Decreasing rollout percentage can remove users who were previously included
  • Local evaluation requires Feature Flags Secure API Key (phs_*) - personal API keys are deprecated
  • Flags load asynchronously - first render always has undefined
  • GeoIP targeting uses server IP by default in posthog-node v3+
  • Experiments need minimum 50 exposures per variant for results
  • Stale flag = 100% rollout + not evaluated in 30 days
  • onFeatureFlags callback receives three parameters: flags, flagVariants, { errorsLoading } (third parameter)
  • External cache providers (Redis, KV) are experimental - Node.js/Python SDKs only

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST always pair useFeatureFlagPayload with useFeatureFlagEnabled or useFeatureFlagVariantKey for experiments - payload hooks don't send exposure events)

(You MUST use the feature flags secure API key (phs_*) for server-side local evaluation - personal API keys are deprecated for this use)

(You MUST handle the undefined state when flags are loading - never assume a flag is immediately available)

(You MUST include flag owner and expiry date in flag metadata - flags without owners become orphaned debt)

(You MUST wrap flag usage in a single function when used in multiple places - prevents orphaned flag code on cleanup)

Failure to follow these rules will cause incorrect experiment results, security vulnerabilities, UI flashing, and technical debt.

</critical_reminders>


Sources

Files (skills)
  • examples
    • core.md 13.8 KB
      # Feature Flags Core Examples
      
      > Essential patterns for PostHog feature flags. Reference from [SKILL.md](../SKILL.md).
      
      **Extended examples:**
      
      - [server-side.md](server-side.md) - Server-side evaluation with posthog-node
      - [development.md](development.md) - Local development overrides and bootstrapping
      
      ---
      
      ## Pattern 1: Client-Side Boolean Flags
      
      Use `useFeatureFlagEnabled` for simple on/off features. Handle the `undefined` loading state.
      
      ### Constants
      
      ```typescript
      // lib/feature-flags.ts
      export const FLAG_NEW_CHECKOUT = "new-checkout-flow";
      export const FLAG_DARK_MODE = "dark-mode-enabled";
      export const FLAG_BETA_DASHBOARD = "beta-dashboard";
      ```
      
      ### Good Example - Handle undefined state
      
      ```typescript
      // components/checkout-button.tsx
      import { useFeatureFlagEnabled } from "posthog-js/react";
      
      import { FLAG_NEW_CHECKOUT } from "../lib/feature-flags";
      
      export const CheckoutButton = () => {
        const isNewCheckout = useFeatureFlagEnabled(FLAG_NEW_CHECKOUT);
      
        // Flag is loading - show nothing or skeleton
        if (isNewCheckout === undefined) {
          return <ButtonSkeleton />;
        }
      
        // Flag resolved - render appropriate UI
        if (isNewCheckout) {
          return <NewCheckoutButton />;
        }
      
        return <LegacyCheckoutButton />;
      };
      ```
      
      **Why good:** Named constant prevents typos, undefined check prevents flash of wrong content, explicit handling of all states
      
      ### Bad Example - No loading state, magic string
      
      ```typescript
      import { useFeatureFlagEnabled } from "posthog-js/react";
      
      export const CheckoutButton = () => {
        // BAD: Magic string for flag key
        // BAD: Treating undefined as false causes flash of legacy UI
        const isNewCheckout = useFeatureFlagEnabled("new-checkout-flow");
      
        if (isNewCheckout) {
          return <NewCheckoutButton />;
        }
      
        return <LegacyCheckoutButton />; // Shows briefly while loading!
      };
      ```
      
      **Why bad:** Magic string causes typos and makes flag hard to find for cleanup, undefined treated as false shows wrong UI briefly before flag loads
      
      ---
      
      ## Pattern 2: Multivariate Flags and Variants
      
      Use `useFeatureFlagVariantKey` for A/B tests with multiple variants.
      
      ### Constants
      
      ```typescript
      // lib/feature-flags.ts
      export const FLAG_PRICING_PAGE = "pricing-page-experiment";
      
      // Variant constants prevent typos
      export const VARIANT_CONTROL = "control";
      export const VARIANT_SIMPLE = "simple";
      export const VARIANT_DETAILED = "detailed";
      ```
      
      ### Good Example - Multivariate flag with loading state
      
      ```typescript
      // components/pricing-page.tsx
      import { useFeatureFlagVariantKey } from "posthog-js/react";
      
      import {
        FLAG_PRICING_PAGE,
        VARIANT_CONTROL,
        VARIANT_SIMPLE,
        VARIANT_DETAILED,
      } from "../lib/feature-flags";
      
      export const PricingPage = () => {
        const variant = useFeatureFlagVariantKey(FLAG_PRICING_PAGE);
      
        // Loading state
        if (variant === undefined) {
          return <PricingPageSkeleton />;
        }
      
        // Render based on variant
        switch (variant) {
          case VARIANT_SIMPLE:
            return <SimplePricing />;
          case VARIANT_DETAILED:
            return <DetailedPricing />;
          case VARIANT_CONTROL:
          default:
            return <ControlPricing />;
        }
      };
      ```
      
      **Why good:** Variant constants prevent typos, switch statement handles all cases, default fallback to control variant, loading state prevents flash
      
      ---
      
      ## Pattern 3: PostHogFeature Component
      
      Use the `PostHogFeature` component for automatic exposure tracking and cleaner code.
      
      ### Good Example - PostHogFeature handles exposure tracking
      
      ```typescript
      import { PostHogFeature } from "posthog-js/react";
      
      import { FLAG_BETA_DASHBOARD } from "../lib/feature-flags";
      
      export const Dashboard = () => {
        return (
          <PostHogFeature
            flag={FLAG_BETA_DASHBOARD}
            match={true} // Show when flag is true
            fallback={<LegacyDashboard />}
          >
            <BetaDashboard />
          </PostHogFeature>
        );
      };
      ```
      
      **Why good:** Automatic exposure event tracking, cleaner JSX, built-in fallback handling, less boilerplate than hooks
      
      ### Good Example - PostHogFeature with variant matching
      
      ```typescript
      import { PostHogFeature } from "posthog-js/react";
      
      import { FLAG_PRICING_PAGE, VARIANT_SIMPLE } from "../lib/feature-flags";
      
      export const PricingSection = () => {
        return (
          <PostHogFeature
            flag={FLAG_PRICING_PAGE}
            match={VARIANT_SIMPLE} // Show when variant is "simple"
            fallback={<DefaultPricing />}
          >
            <SimplePricing />
          </PostHogFeature>
        );
      };
      ```
      
      **Why good:** Match specific variant values, automatic exposure tracking for experiment
      
      ---
      
      ## Pattern 4: Feature Flag Payloads for Remote Configuration
      
      Use `useFeatureFlagPayload` for dynamic configuration. Always pair with `useFeatureFlagEnabled` for experiments.
      
      ### Constants
      
      ```typescript
      // lib/feature-flags.ts
      export const FLAG_BANNER_CONFIG = "homepage-banner";
      
      // Default payload for fallback
      export const DEFAULT_BANNER_CONFIG = {
        title: "Welcome",
        ctaText: "Get Started",
        ctaUrl: "/signup",
        backgroundColor: "#3B82F6",
      };
      ```
      
      ### Good Example - Payload with enabled check for experiment tracking
      
      ```typescript
      // components/homepage-banner.tsx
      import { useFeatureFlagEnabled, useFeatureFlagPayload } from "posthog-js/react";
      
      import { FLAG_BANNER_CONFIG, DEFAULT_BANNER_CONFIG } from "../lib/feature-flags";
      
      interface BannerConfig {
        title: string;
        ctaText: string;
        ctaUrl: string;
        backgroundColor: string;
      }
      
      export const HomepageBanner = () => {
        // CRITICAL: Call useFeatureFlagEnabled to send exposure event
        const isEnabled = useFeatureFlagEnabled(FLAG_BANNER_CONFIG);
      
        // Get the payload configuration
        const payload = useFeatureFlagPayload(FLAG_BANNER_CONFIG) as BannerConfig | undefined;
      
        // Not enabled or loading
        if (!isEnabled) {
          return null;
        }
      
        // Use payload or fall back to defaults
        const config = payload ?? DEFAULT_BANNER_CONFIG;
      
        return (
          <div style={{ backgroundColor: config.backgroundColor }}>
            <h2>{config.title}</h2>
            <a href={config.ctaUrl}>{config.ctaText}</a>
          </div>
        );
      };
      ```
      
      **Why good:** useFeatureFlagEnabled sends $feature_flag_called event for experiment tracking, payload provides dynamic config, default config prevents undefined errors
      
      ### Bad Example - Payload without enabled check
      
      ```typescript
      export const HomepageBanner = () => {
        // BAD: Only using payload - no $feature_flag_called event sent!
        // Experiments won't track exposure correctly
        const payload = useFeatureFlagPayload("homepage-banner");
      
        if (!payload) return null;
      
        return <Banner {...payload} />;
      };
      ```
      
      **Why bad:** useFeatureFlagPayload alone does NOT send exposure events, A/B test results will be incorrect, PostHog won't know user was exposed to experiment
      
      ---
      
      ## Pattern 5: A/B Testing with Experiments
      
      Run experiments to measure feature impact.
      
      ### Setting Up an Experiment
      
      ```typescript
      // lib/feature-flags.ts
      
      // Experiment flag with variants
      export const FLAG_SIGNUP_EXPERIMENT = "signup-flow-experiment";
      export const VARIANT_CONTROL = "control";
      export const VARIANT_STREAMLINED = "streamlined";
      export const VARIANT_SOCIAL_FIRST = "social-first";
      
      // Minimum exposures for statistical significance
      export const MIN_EXPERIMENT_EXPOSURES = 50;
      ```
      
      ### Good Example - Experiment with variant tracking
      
      ```typescript
      // components/signup-flow.tsx
      import { useFeatureFlagVariantKey } from "posthog-js/react";
      
      import {
        FLAG_SIGNUP_EXPERIMENT,
        VARIANT_CONTROL,
        VARIANT_STREAMLINED,
        VARIANT_SOCIAL_FIRST,
      } from "../lib/feature-flags";
      
      export const SignupFlow = () => {
        const variant = useFeatureFlagVariantKey(FLAG_SIGNUP_EXPERIMENT);
      
        // Handle loading
        if (variant === undefined) {
          return <SignupSkeleton />;
        }
      
        // Render variant - PostHog automatically tracks exposure
        switch (variant) {
          case VARIANT_STREAMLINED:
            return <StreamlinedSignup />;
          case VARIANT_SOCIAL_FIRST:
            return <SocialFirstSignup />;
          case VARIANT_CONTROL:
          default:
            return <ControlSignup />;
        }
      };
      ```
      
      ### Tracking Experiment Goals
      
      ```typescript
      // components/signup-success.tsx
      import { usePostHog } from "posthog-js/react";
      
      const EVENT_SIGNUP_COMPLETED = "signup_completed";
      
      export const SignupSuccess = () => {
        const posthog = usePostHog();
      
        useEffect(() => {
          // Track conversion event - PostHog links to experiment exposure
          posthog.capture(EVENT_SIGNUP_COMPLETED, {
            method: "email", // Additional properties for analysis
          });
        }, [posthog]);
      
        return <SuccessMessage />;
      };
      ```
      
      **Why good:** useFeatureFlagVariantKey sends exposure event, capture sends conversion event, PostHog calculates statistical significance automatically
      
      **Experiment requirements:**
      
      - Minimum 50 exposures per variant for results
      - Define primary metric before starting
      - Don't peek at results early (statistical errors)
      - Run for predetermined duration
      
      ---
      
      ## Pattern 6: Gradual Rollouts and User Targeting
      
      Use percentage-based rollouts for safe feature releases.
      
      ### Rollout Strategy
      
      ```typescript
      // lib/feature-flags.ts
      
      // Document rollout plan in constants
      export const FLAG_NEW_PAYMENT_FLOW = "new-payment-flow";
      
      // Rollout phases (configured in PostHog dashboard)
      // Phase 1: 5% - Internal testing
      // Phase 2: 25% - Beta users
      // Phase 3: 50% - Half of users
      // Phase 4: 100% - General availability
      ```
      
      ### Implementation
      
      ```typescript
      // components/payment-form.tsx
      import { useFeatureFlagEnabled } from "posthog-js/react";
      
      import { FLAG_NEW_PAYMENT_FLOW } from "../lib/feature-flags";
      
      export const PaymentForm = () => {
        const isNewFlow = useFeatureFlagEnabled(FLAG_NEW_PAYMENT_FLOW);
      
        // Loading state
        if (isNewFlow === undefined) {
          return <PaymentFormSkeleton />;
        }
      
        // PostHog assigns users deterministically based on distinct_id
        // Same user always gets same variant (sticky assignment)
        if (isNewFlow) {
          return <NewPaymentFlow />;
        }
      
        return <LegacyPaymentFlow />;
      };
      ```
      
      **Why good:** Deterministic assignment ensures consistent experience per user, gradual rollout catches issues before 100% exposure
      
      **Rollout best practices:**
      
      1. Start at 5% and monitor error rates
      2. Increase to 25% after 24 hours if stable
      3. Increase to 50% after another 24 hours
      4. Roll out to 100% if all metrics look good
      5. Keep kill switch ready for instant rollback
      
      ### User and Cohort Targeting
      
      Target specific users based on properties or cohorts.
      
      ```typescript
      // lib/posthog-server.ts
      
      export async function checkFeatureForUser(
        userId: string,
        userProperties: {
          email: string;
          plan: string;
          company?: string;
        },
      ) {
        const isEnabled = await posthog.isFeatureEnabled(
          FLAG_ENTERPRISE_FEATURE,
          userId,
          {
            // Properties used for targeting rules in PostHog
            personProperties: {
              email: userProperties.email,
              plan: userProperties.plan,
              company: userProperties.company,
            },
          },
        );
      
        return isEnabled;
      }
      ```
      
      **Why good:** Person properties enable targeting (e.g., plan = "enterprise"), server-side evaluation prevents client manipulation of targeting
      
      ### Cohort Targeting in PostHog Dashboard
      
      ```markdown
      ## PostHog Dashboard Configuration
      
      1. Create cohort: "Beta Users"
         - Condition: email contains "@beta.example.com" OR
         - Condition: has property beta_tester = true
      
      2. Create feature flag: "beta-feature"
         - Release condition: cohort = "Beta Users"
         - Rollout: 100%
      
      3. Add second condition set:
         - Condition: All users
         - Rollout: 0% (default off for everyone else)
      ```
      
      **Why good:** Cohorts are reusable across flags, easy to add/remove users from cohort, phased rollouts just swap cohorts
      
      ---
      
      ## Pattern 7: Flag Cleanup and Lifecycle Management
      
      Plan for flag removal from day one. Stale flags become technical debt.
      
      ### Flag Documentation Pattern
      
      ```typescript
      // lib/feature-flags.ts
      
      /**
       * Flag: new-checkout-flow
       * Owner: @john-doe
       * Created: 2025-01-15
       * Expected Removal: 2025-02-15 (after 30 days at 100%)
       * Purpose: Test streamlined checkout with fewer steps
       * Rollout Status: 50% as of 2025-01-20
       */
      export const FLAG_NEW_CHECKOUT = "new-checkout-flow";
      
      /**
       * Flag: holiday-banner
       * Owner: @marketing-team
       * Created: 2025-12-01
       * Expected Removal: 2025-01-02 (seasonal)
       * Purpose: Holiday promotion banner
       */
      export const FLAG_HOLIDAY_BANNER = "holiday-banner";
      ```
      
      **Why good:** Documentation makes ownership clear, expected removal date prevents flags from becoming stale, purpose helps future developers understand intent
      
      ### Wrapper Pattern for Easy Cleanup
      
      ```typescript
      // lib/feature-flags.ts
      
      // Good Example - Single wrapper function for flag
      export function isNewCheckoutEnabled(flagValue: boolean | undefined): boolean {
        // Single point of truth for this flag's behavior
        // When removing flag, only update this function
        return flagValue === true;
      }
      
      // Usage in components
      const flagValue = useFeatureFlagEnabled(FLAG_NEW_CHECKOUT);
      if (isNewCheckoutEnabled(flagValue)) {
        // New checkout code
      }
      
      // When removing flag, change to:
      export function isNewCheckoutEnabled(_flagValue: boolean | undefined): boolean {
        return true; // Always enabled, ready for code cleanup
      }
      ```
      
      **Why good:** Single function to update when removing flag, grep for function name finds all usages, gradual cleanup possible
      
      ### Stale Flag Detection
      
      ```typescript
      // scripts/check-stale-flags.ts
      // Run in CI to detect stale flags
      
      /**
       * PostHog marks a flag as stale when:
       * 1. Rolled out to 100% AND
       * 2. Not evaluated in last 30 days
       *
       * Check stale flags in PostHog dashboard:
       * Feature Flags > Filter by "Stale"
       */
      
      const STALE_FLAGS_QUERY = `
        SELECT key, created_at, last_evaluated_at
        FROM feature_flags
        WHERE rollout_percentage = 100
          AND last_evaluated_at < NOW() - INTERVAL '30 days'
      `;
      ```
      
      **Best practices for flag cleanup:**
      
      1. Set expiry date when creating flag
      2. Assign owner who is responsible for removal
      3. Create cleanup ticket when flag hits 100%
      4. Wrap flag in single function for easy grep
      5. Run weekly "flag debt" review
      6. Archive flag in PostHog before removing code
      
      ---
      
      _Back to [SKILL.md](../SKILL.md) | Extended examples: [server-side.md](server-side.md) | [development.md](development.md)_
      
    • development.md 3.3 KB
      # Local Development Overrides Examples
      
      > Override flags locally without affecting other users. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern: Local Development Overrides
      
      Override flags locally without affecting other users.
      
      ### Method 1: Toolbar Override (Browser)
      
      ```typescript
      // Use PostHog toolbar in development
      // 1. Enable toolbar in PostHog project settings
      // 2. Click feature flags icon in toolbar
      // 3. Toggle flags on/off for your session only
      
      // This only affects YOUR browser, not other users
      // Does NOT affect server-side evaluation
      ```
      
      ### Method 2: Code Override (Development Only)
      
      ```typescript
      // lib/posthog-client.ts
      import posthog from "posthog-js";
      
      const IS_DEVELOPMENT = process.env.NODE_ENV === "development";
      
      // Good Example - Development-only flag overrides
      export function initPostHog() {
        posthog.init(process.env.POSTHOG_PUBLIC_KEY!, {
          api_host: process.env.POSTHOG_PUBLIC_HOST,
          loaded: (posthog) => {
            if (IS_DEVELOPMENT) {
              // Override specific flags for local development
              posthog.featureFlags.overrideFeatureFlags({
                "new-checkout-flow": true,
                "beta-dashboard": "variant-a",
              });
            }
          },
        });
      }
      ```
      
      **Why good:** IS_DEVELOPMENT check ensures production isn't affected, overrideFeatureFlags works immediately, supports both boolean and variant values
      
      ### Method 3: Bootstrapping for SSR
      
      ```typescript
      // lib/posthog-client.ts
      import posthog from "posthog-js";
      
      // Good Example - Bootstrap flags for immediate availability
      export function initPostHogWithBootstrap(
        bootstrapFlags: Record<string, boolean | string>,
        distinctId: string,
      ) {
        posthog.init(process.env.POSTHOG_PUBLIC_KEY!, {
          api_host: process.env.POSTHOG_PUBLIC_HOST,
          bootstrap: {
            featureFlags: bootstrapFlags,
            distinctID: distinctId, // Match server-side ID
            isIdentifiedID: true, // True if using email/DB ID, false for anonymous
          },
        });
      }
      
      // Usage in server component
      // Fetch flags server-side, pass to client for immediate availability
      ```
      
      **Why good:** Bootstrap eliminates flash of wrong content, flags available immediately on page load, essential for SSR/SSG, distinctID ensures consistency with server evaluation
      
      ---
      
      ### Method 4: onFeatureFlags Callback
      
      ```typescript
      // lib/posthog-client.ts
      import posthog from "posthog-js";
      
      // Good Example - Wait for flags to load before using
      export function initPostHogWithCallback() {
        posthog.init(process.env.POSTHOG_PUBLIC_KEY!, {
          api_host: process.env.POSTHOG_PUBLIC_HOST,
          loaded: (posthog) => {
            // Called when PostHog SDK is loaded
            posthog.onFeatureFlags((flags, flagVariants, { errorsLoading }) => {
              // flags: string[] - list of flag keys
              // flagVariants: Record<string, string | boolean>
              // errorsLoading: boolean | undefined - true if request failed/timed out
      
              if (errorsLoading) {
                console.warn("Feature flags failed to load, using defaults");
                return;
              }
      
              // Flags are now available
              console.log("Feature flags loaded:", flagVariants);
            });
          },
        });
      }
      ```
      
      **Why good:** `errorsLoading` parameter enables graceful degradation, callback fires on every flag reload, essential for handling network failures
      
      ---
      
      _Back to [SKILL.md](../SKILL.md) | [core.md](core.md)_
      
    • experiments.md 2.3 KB
      # A/B Testing with Experiments Examples
      
      > A/B testing patterns with PostHog experiments. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern: A/B Testing with Experiments
      
      Run experiments to measure feature impact.
      
      ### Setting Up an Experiment
      
      ```typescript
      // lib/feature-flags.ts
      
      // Experiment flag with variants
      export const FLAG_SIGNUP_EXPERIMENT = "signup-flow-experiment";
      export const VARIANT_CONTROL = "control";
      export const VARIANT_STREAMLINED = "streamlined";
      export const VARIANT_SOCIAL_FIRST = "social-first";
      
      // Minimum exposures for statistical significance
      export const MIN_EXPERIMENT_EXPOSURES = 50;
      ```
      
      ### Good Example - Experiment with variant tracking
      
      ```typescript
      // components/signup-flow.tsx
      import { useFeatureFlagVariantKey } from "posthog-js/react";
      
      import {
        FLAG_SIGNUP_EXPERIMENT,
        VARIANT_CONTROL,
        VARIANT_STREAMLINED,
        VARIANT_SOCIAL_FIRST,
      } from "@/lib/feature-flags";
      
      export const SignupFlow = () => {
        const variant = useFeatureFlagVariantKey(FLAG_SIGNUP_EXPERIMENT);
      
        // Handle loading
        if (variant === undefined) {
          return <SignupSkeleton />;
        }
      
        // Render variant - PostHog automatically tracks exposure
        switch (variant) {
          case VARIANT_STREAMLINED:
            return <StreamlinedSignup />;
          case VARIANT_SOCIAL_FIRST:
            return <SocialFirstSignup />;
          case VARIANT_CONTROL:
          default:
            return <ControlSignup />;
        }
      };
      ```
      
      ### Tracking Experiment Goals
      
      ```typescript
      // components/signup-success.tsx
      import { usePostHog } from "posthog-js/react";
      
      const EVENT_SIGNUP_COMPLETED = "signup_completed";
      
      export const SignupSuccess = () => {
        const posthog = usePostHog();
      
        useEffect(() => {
          // Track conversion event - PostHog links to experiment exposure
          posthog.capture(EVENT_SIGNUP_COMPLETED, {
            method: "email", // Additional properties for analysis
          });
        }, [posthog]);
      
        return <SuccessMessage />;
      };
      ```
      
      **Why good:** useFeatureFlagVariantKey sends exposure event, capture sends conversion event, PostHog calculates statistical significance automatically
      
      **Experiment requirements:**
      
      - Minimum 50 exposures per variant for results
      - Define primary metric before starting
      - Don't peek at results early (statistical errors)
      - Run for predetermined duration
      
      ---
      
      _Back to [SKILL.md](../SKILL.md) | [core.md](core.md)_
      
    • lifecycle.md 2.6 KB
      # Flag Cleanup and Lifecycle Management Examples
      
      > Plan for flag removal from day one. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern: Flag Cleanup and Lifecycle Management
      
      Plan for flag removal from day one. Stale flags become technical debt.
      
      ### Flag Documentation Pattern
      
      ```typescript
      // lib/feature-flags.ts
      
      /**
       * Flag: new-checkout-flow
       * Owner: @john-doe
       * Created: 2025-01-15
       * Expected Removal: 2025-02-15 (after 30 days at 100%)
       * Purpose: Test streamlined checkout with fewer steps
       * Rollout Status: 50% as of 2025-01-20
       */
      export const FLAG_NEW_CHECKOUT = "new-checkout-flow";
      
      /**
       * Flag: holiday-banner
       * Owner: @marketing-team
       * Created: 2025-12-01
       * Expected Removal: 2025-01-02 (seasonal)
       * Purpose: Holiday promotion banner
       */
      export const FLAG_HOLIDAY_BANNER = "holiday-banner";
      ```
      
      **Why good:** Documentation makes ownership clear, expected removal date prevents flags from becoming stale, purpose helps future developers understand intent
      
      ### Wrapper Pattern for Easy Cleanup
      
      ```typescript
      // lib/feature-flags.ts
      
      // Good Example - Single wrapper function for flag
      export function isNewCheckoutEnabled(flagValue: boolean | undefined): boolean {
        // Single point of truth for this flag's behavior
        // When removing flag, only update this function
        return flagValue === true;
      }
      
      // Usage in components
      const flagValue = useFeatureFlagEnabled(FLAG_NEW_CHECKOUT);
      if (isNewCheckoutEnabled(flagValue)) {
        // New checkout code
      }
      
      // When removing flag, change to:
      export function isNewCheckoutEnabled(_flagValue: boolean | undefined): boolean {
        return true; // Always enabled, ready for code cleanup
      }
      ```
      
      **Why good:** Single function to update when removing flag, grep for function name finds all usages, gradual cleanup possible
      
      ### Stale Flag Detection
      
      ```typescript
      // scripts/check-stale-flags.ts
      // Run in CI to detect stale flags
      
      /**
       * PostHog marks a flag as stale when:
       * 1. Rolled out to 100% AND
       * 2. Not evaluated in last 30 days
       *
       * Check stale flags in PostHog dashboard:
       * Feature Flags > Filter by "Stale"
       */
      
      const STALE_FLAGS_QUERY = `
        SELECT key, created_at, last_evaluated_at
        FROM feature_flags
        WHERE rollout_percentage = 100
          AND last_evaluated_at < NOW() - INTERVAL '30 days'
      `;
      ```
      
      **Best practices for flag cleanup:**
      
      1. Set expiry date when creating flag
      2. Assign owner who is responsible for removal
      3. Create cleanup ticket when flag hits 100%
      4. Wrap flag in single function for easy grep
      5. Run weekly "flag debt" review
      6. Archive flag in PostHog before removing code
      
      ---
      
      _Back to [SKILL.md](../SKILL.md) | [core.md](core.md)_
      
    • payloads.md 2.3 KB
      # Feature Flag Payloads Examples
      
      > Remote configuration with JSON payloads. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern: Feature Flag Payloads for Remote Configuration
      
      Use `useFeatureFlagPayload` for dynamic configuration. Always pair with `useFeatureFlagEnabled` for experiments.
      
      ### Constants
      
      ```typescript
      // lib/feature-flags.ts
      export const FLAG_BANNER_CONFIG = "homepage-banner";
      
      // Default payload for fallback
      export const DEFAULT_BANNER_CONFIG = {
        title: "Welcome",
        ctaText: "Get Started",
        ctaUrl: "/signup",
        backgroundColor: "#3B82F6",
      };
      ```
      
      ### Good Example - Payload with enabled check for experiment tracking
      
      ```typescript
      // components/homepage-banner.tsx
      import { useFeatureFlagEnabled, useFeatureFlagPayload } from "posthog-js/react";
      
      import { FLAG_BANNER_CONFIG, DEFAULT_BANNER_CONFIG } from "@/lib/feature-flags";
      
      interface BannerConfig {
        title: string;
        ctaText: string;
        ctaUrl: string;
        backgroundColor: string;
      }
      
      export const HomepageBanner = () => {
        // CRITICAL: Call useFeatureFlagEnabled to send exposure event
        const isEnabled = useFeatureFlagEnabled(FLAG_BANNER_CONFIG);
      
        // Get the payload configuration
        const payload = useFeatureFlagPayload(FLAG_BANNER_CONFIG) as BannerConfig | undefined;
      
        // Not enabled or loading
        if (!isEnabled) {
          return null;
        }
      
        // Use payload or fall back to defaults
        const config = payload ?? DEFAULT_BANNER_CONFIG;
      
        return (
          <div style={{ backgroundColor: config.backgroundColor }}>
            <h2>{config.title}</h2>
            <a href={config.ctaUrl}>{config.ctaText}</a>
          </div>
        );
      };
      ```
      
      **Why good:** useFeatureFlagEnabled sends $feature_flag_called event for experiment tracking, payload provides dynamic config, default config prevents undefined errors
      
      ### Bad Example - Payload without enabled check
      
      ```typescript
      export const HomepageBanner = () => {
        // BAD: Only using payload - no $feature_flag_called event sent!
        // Experiments won't track exposure correctly
        const payload = useFeatureFlagPayload("homepage-banner");
      
        if (!payload) return null;
      
        return <Banner {...payload} />;
      };
      ```
      
      **Why bad:** useFeatureFlagPayload alone does NOT send exposure events, A/B test results will be incorrect, PostHog won't know user was exposed to experiment
      
      ---
      
      _Back to [SKILL.md](../SKILL.md) | [core.md](core.md)_
      
    • rollouts.md 2.9 KB
      # Gradual Rollouts and Targeting Examples
      
      > Gradual rollouts and user targeting patterns. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern 1: Gradual Rollouts
      
      Use percentage-based rollouts for safe feature releases.
      
      ### Rollout Strategy
      
      ```typescript
      // lib/feature-flags.ts
      
      // Document rollout plan in constants
      export const FLAG_NEW_PAYMENT_FLOW = "new-payment-flow";
      
      // Rollout phases (configured in PostHog dashboard)
      // Phase 1: 5% - Internal testing
      // Phase 2: 25% - Beta users
      // Phase 3: 50% - Half of users
      // Phase 4: 100% - General availability
      ```
      
      ### Implementation
      
      ```typescript
      // components/payment-form.tsx
      import { useFeatureFlagEnabled } from "posthog-js/react";
      
      import { FLAG_NEW_PAYMENT_FLOW } from "@/lib/feature-flags";
      
      export const PaymentForm = () => {
        const isNewFlow = useFeatureFlagEnabled(FLAG_NEW_PAYMENT_FLOW);
      
        // Loading state
        if (isNewFlow === undefined) {
          return <PaymentFormSkeleton />;
        }
      
        // PostHog assigns users deterministically based on distinct_id
        // Same user always gets same variant (sticky assignment)
        if (isNewFlow) {
          return <NewPaymentFlow />;
        }
      
        return <LegacyPaymentFlow />;
      };
      ```
      
      **Why good:** Deterministic assignment ensures consistent experience per user, gradual rollout catches issues before 100% exposure
      
      **Rollout best practices:**
      
      1. Start at 5% and monitor error rates
      2. Increase to 25% after 24 hours if stable
      3. Increase to 50% after another 24 hours
      4. Roll out to 100% if all metrics look good
      5. Keep kill switch ready for instant rollback
      
      ---
      
      ## Pattern 2: User and Cohort Targeting
      
      Target specific users based on properties or cohorts.
      
      ### Good Example - Targeting with person properties
      
      ```typescript
      // lib/posthog-server.ts
      
      export async function checkFeatureForUser(
        userId: string,
        userProperties: {
          email: string;
          plan: string;
          company?: string;
        },
      ) {
        const isEnabled = await posthog.isFeatureEnabled(
          FLAG_ENTERPRISE_FEATURE,
          userId,
          {
            // Properties used for targeting rules in PostHog
            personProperties: {
              email: userProperties.email,
              plan: userProperties.plan,
              company: userProperties.company,
            },
          },
        );
      
        return isEnabled;
      }
      ```
      
      **Why good:** Person properties enable targeting (e.g., plan = "enterprise"), server-side evaluation prevents client manipulation of targeting
      
      ### Cohort Targeting in PostHog Dashboard
      
      ```markdown
      ## PostHog Dashboard Configuration
      
      1. Create cohort: "Beta Users"
         - Condition: email contains "@beta.example.com" OR
         - Condition: has property beta_tester = true
      
      2. Create feature flag: "beta-feature"
         - Release condition: cohort = "Beta Users"
         - Rollout: 100%
      
      3. Add second condition set:
         - Condition: All users
         - Rollout: 0% (default off for everyone else)
      ```
      
      **Why good:** Cohorts are reusable across flags, easy to add/remove users from cohort, phased rollouts just swap cohorts
      
      ---
      
      _Back to [SKILL.md](../SKILL.md) | [core.md](core.md)_
      
    • server-side.md 7.2 KB
      # Server-Side Flag Evaluation Examples
      
      > Server-side evaluation with posthog-node. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern: Server-Side Flag Evaluation
      
      Use `posthog-node` for server-side evaluation. Use local evaluation for performance.
      
      ### Setup
      
      ```typescript
      // lib/posthog-server.ts
      import { PostHog } from "posthog-node";
      
      const POSTHOG_POLL_INTERVAL_MS = 30_000; // 30 seconds (SDK default)
      
      // Initialize with local evaluation
      // Use Feature Flags Secure API Key (phs_*) from project settings > Feature Flags tab
      // Personal API keys are DEPRECATED for local evaluation
      export const posthog = new PostHog(process.env.POSTHOG_API_KEY!, {
        host: process.env.POSTHOG_HOST || "https://us.i.posthog.com",
        // Enable local evaluation with feature flags secure key (phs_*)
        personalApiKey: process.env.POSTHOG_FEATURE_FLAGS_KEY,
        // Poll for flag definition updates (default: 30000ms / 30 seconds)
        featureFlagsPollingInterval: POSTHOG_POLL_INTERVAL_MS,
      });
      ```
      
      **Note:** Get the Feature Flags Secure API Key from PostHog Project Settings > Feature Flags tab. The key starts with `phs_`. Personal API keys are deprecated for local evaluation.
      
      ### Good Example - Server-side flag evaluation in an API handler
      
      ```typescript
      // api/dashboard.ts (adapt to your server framework)
      import { posthog } from "../lib/posthog-server";
      import { FLAG_BETA_DASHBOARD } from "../lib/feature-flags";
      
      export async function handleGetDashboard(request: Request) {
        const userId = request.headers.get("x-user-id");
      
        if (!userId) {
          return new Response(JSON.stringify({ error: "Unauthorized" }), {
            status: 401,
          });
        }
      
        // Evaluate flag server-side with user context
        const isBetaDashboard = await posthog.isFeatureEnabled(
          FLAG_BETA_DASHBOARD,
          userId,
          {
            // Provide person properties for targeting rules
            personProperties: {
              email: request.headers.get("x-user-email"),
              plan: request.headers.get("x-user-plan"),
            },
          },
        );
      
        const dashboard = isBetaDashboard
          ? { dashboard: "beta" }
          : { dashboard: "stable" };
      
        return new Response(JSON.stringify(dashboard));
      }
      ```
      
      **Why good:** Local evaluation reduces latency (500ms to 50ms), personProperties enable targeting, server-side prevents client manipulation
      
      ### Good Example - Local-only evaluation for performance
      
      ```typescript
      const flag = await posthog.isFeatureEnabled(FLAG_BETA_DASHBOARD, userId, {
        personProperties: { plan: "enterprise" },
        // Only evaluate locally - don't fall back to server
        onlyEvaluateLocally: true,
      });
      
      // Returns undefined if local evaluation fails
      if (flag === undefined) {
        // Fall back to default behavior
        return defaultDashboard();
      }
      ```
      
      **Why good:** onlyEvaluateLocally prevents server roundtrips, explicit undefined handling for fallback
      
      ### Bad Example - Server-side without local evaluation
      
      ```typescript
      import { PostHog } from "posthog-node";
      
      // BAD: No personalApiKey = no local evaluation
      const posthog = new PostHog(process.env.POSTHOG_API_KEY!);
      
      // BAD: Every call makes a network request (500ms latency)
      const flag = await posthog.isFeatureEnabled("beta-dashboard", userId);
      ```
      
      **Why bad:** No local evaluation = network request per flag check, high latency (500ms vs 10-50ms), increases PostHog API costs
      
      ---
      
      ## Pattern: Distributed/Stateless Environments (Serverless/Edge)
      
      In serverless/edge environments, the default in-memory cache causes performance issues due to per-request initialization. Use an external cache provider to share flag definitions.
      
      ### The Problem
      
      ```typescript
      // BAD: In Lambda/Edge, each invocation may cold start
      // Flag definitions are fetched on EVERY cold start = slow + expensive
      const posthog = new PostHog(process.env.POSTHOG_API_KEY!, {
        personalApiKey: process.env.POSTHOG_FEATURE_FLAGS_KEY,
      });
      // Cold start: +500ms to fetch definitions
      ```
      
      ### Solution 1: External Cache (Redis) - Node.js/Python Only
      
      ```typescript
      // lib/posthog-server-redis.ts
      import { PostHog } from "posthog-node";
      import type {
        FlagDefinitionCacheProvider,
        FlagDefinitionCacheData,
      } from "posthog-node";
      import { createClient } from "redis";
      
      const CACHE_TTL_SECONDS = 300; // 5 minutes
      const CACHE_KEY = "posthog:flag-definitions";
      const LOCK_KEY = "posthog:flag-definitions:lock";
      const LOCK_TTL_SECONDS = 60;
      
      // Create a Redis-based cache provider (experimental feature)
      const createRedisCache = (redisUrl: string): FlagDefinitionCacheProvider => {
        const client = createClient({ url: redisUrl });
        client.connect();
      
        return {
          // Retrieve cached flag definitions
          async getFlagDefinitions(): Promise<FlagDefinitionCacheData | undefined> {
            const data = await client.get(CACHE_KEY);
            return data ? JSON.parse(data) : undefined;
          },
      
          // Only one instance should fetch definitions at a time
          async shouldFetchFlagDefinitions(): Promise<boolean> {
            const acquired = await client.set(LOCK_KEY, "1", {
              NX: true,
              EX: LOCK_TTL_SECONDS,
            });
            return acquired !== null;
          },
      
          // Store definitions after a successful fetch
          async onFlagDefinitionsReceived(
            data: FlagDefinitionCacheData,
          ): Promise<void> {
            await client.setEx(CACHE_KEY, CACHE_TTL_SECONDS, JSON.stringify(data));
          },
      
          // Release locks and close connections
          async shutdown(): Promise<void> {
            await client.del(LOCK_KEY);
            await client.quit();
          },
        };
      };
      
      export const posthog = new PostHog(process.env.POSTHOG_API_KEY!, {
        host: process.env.POSTHOG_HOST || "https://us.i.posthog.com",
        personalApiKey: process.env.POSTHOG_FEATURE_FLAGS_KEY,
        // Use Redis for shared flag definitions across instances
        flagDefinitionCacheProvider: createRedisCache(process.env.REDIS_URL!),
      });
      ```
      
      **Why good:** All Lambda/Edge instances share one cache, `shouldFetchFlagDefinitions` ensures only one instance fetches, eliminates cold start penalty
      
      ### Solution 2: Split Read/Write Pattern (Cloudflare KV)
      
      For storage backends without atomic locking (Cloudflare KV), use a cron job to write and workers to read.
      
      ```typescript
      // scheduled-worker.ts (cron job - writes to KV)
      import { PostHog } from "posthog-node";
      
      const CACHE_KEY = "posthog-flags";
      
      export default {
        async scheduled(event: ScheduledEvent, env: Env) {
          const posthog = new PostHog(env.POSTHOG_API_KEY, {
            personalApiKey: env.POSTHOG_FEATURE_FLAGS_KEY,
          });
      
          // Fetch latest flag definitions
          const definitions = await posthog.getAllFlags("system");
      
          // Write to KV for all workers to read
          await env.FLAGS_KV.put(CACHE_KEY, JSON.stringify(definitions));
        },
      };
      ```
      
      ```typescript
      // request-worker.ts (handles requests - reads from KV)
      const CACHE_KEY = "posthog-flags";
      
      export default {
        async fetch(request: Request, env: Env) {
          // Read flag definitions from KV (no API call)
          const definitions = await env.FLAGS_KV.get(CACHE_KEY, "json");
      
          // Evaluate locally without network request
          const isEnabled = definitions?.["beta-feature"] === true;
      
          return new Response(JSON.stringify({ isEnabled }));
        },
      };
      ```
      
      **Why good:** Zero network requests for flag evaluation, cron controls update frequency, works with any KV-like storage
      
      **Note:** External cache providers are currently experimental and available in Node.js and Python SDKs only.
      
      ---
      
      _Back to [SKILL.md](../SKILL.md) | [core.md](core.md)_
      
  • reference.md 4 KB
    # Feature Flags Reference
    
    > Decision frameworks, anti-patterns, and red flags for PostHog feature flags.
    
    ---
    
    ## Decision Framework
    
    ### Flag Type Selection
    
    ```
    What kind of feature flag do you need?
    
    Is it a simple on/off switch?
    +-- YES -> Boolean flag
    |   +-- Need to target specific users?
    |   |   +-- YES -> Add release conditions (cohorts, properties)
    |   |   +-- NO -> Use percentage rollout only
    |   +-- Need remote configuration?
    |       +-- YES -> Add JSON payload to flag
    |       +-- NO -> Boolean value is sufficient
    +-- NO -> Is it an A/B test with variants?
        +-- YES -> Multivariate flag
        |   +-- How many variants?
        |       +-- 2 (A/B) -> control + test
        |       +-- 3+ (MVT) -> control + multiple tests
        +-- NO -> Is it for experiments with metrics?
            +-- YES -> Create as Experiment in PostHog
            +-- NO -> Consider if you really need a flag
    ```
    
    ### Client vs Server Evaluation
    
    ```
    Where should you evaluate the flag?
    
    Is the feature visible in UI?
    +-- YES -> Client-side (useFeatureFlagEnabled)
    |   +-- Need to prevent content flash?
    |       +-- YES -> Bootstrap flags from server
    |       +-- NO -> Handle undefined loading state
    +-- NO -> Is it API/backend behavior?
        +-- YES -> Server-side (posthog-node)
        |   +-- High traffic endpoint?
        |       +-- YES -> Use local evaluation
        |       +-- NO -> Regular evaluation is fine
        +-- NO -> Evaluate in component
    
    Is the flag security-sensitive?
    +-- YES -> Server-side only (prevents client manipulation)
    +-- NO -> Either works
    ```
    
    ### Quick Reference
    
    | Use Case           | Flag Type         | Evaluation  |
    | ------------------ | ----------------- | ----------- |
    | Gradual rollout    | Boolean           | Client      |
    | A/B test           | Multivariate      | Client      |
    | Kill switch        | Boolean           | Both        |
    | Beta access        | Boolean + cohort  | Client      |
    | API behavior       | Boolean           | Server      |
    | Remote config      | Boolean + payload | Client      |
    | Pricing experiment | Multivariate      | Client      |
    | Security feature   | Boolean           | Server only |
    
    ---
    
    ## Anti-Patterns
    
    ### Using Payload Without Enabled Check
    
    ```typescript
    // ANTI-PATTERN: Payload alone doesn't send exposure event
    const payload = useFeatureFlagPayload("experiment-flag");
    // PostHog won't know user was exposed!
    
    // CORRECT: Always pair with enabled check
    const isEnabled = useFeatureFlagEnabled("experiment-flag");
    const payload = useFeatureFlagPayload("experiment-flag");
    ```
    
    **Why it's wrong:** Experiments require exposure events to calculate results. Payload hook doesn't send them.
    
    ---
    
    ### Ignoring Loading State
    
    ```typescript
    // ANTI-PATTERN: Treating undefined as false
    const isEnabled = useFeatureFlagEnabled("new-feature");
    if (isEnabled) { /* new */ } else { /* old - shows briefly! */ }
    
    // CORRECT: Handle all three states
    if (isEnabled === undefined) return <Skeleton />;
    if (isEnabled) return <NewFeature />;
    return <OldFeature />;
    ```
    
    **Why it's wrong:** Users see flash of old UI before flag loads, looks buggy.
    
    ---
    
    ### Flags Without Owners
    
    ```typescript
    // ANTI-PATTERN: No documentation
    export const FLAG_SOMETHING = "some-feature";
    
    // CORRECT: Document owner and lifecycle
    /**
     * Owner: @jane-doe
     * Created: 2025-01-15
     * Expected Removal: 2025-02-15
     */
    export const FLAG_SOMETHING = "some-feature";
    ```
    
    **Why it's wrong:** Orphaned flags become permanent technical debt.
    
    ---
    
    ### Flag Sprawl Across Codebase
    
    ```typescript
    // ANTI-PATTERN: Flag checked in multiple places
    // file1.tsx
    if (useFeatureFlagEnabled("new-feature")) { ... }
    // file2.tsx
    if (useFeatureFlagEnabled("new-feature")) { ... }
    // file3.tsx
    if (useFeatureFlagEnabled("new-feature")) { ... }
    
    // CORRECT: Wrapper function in one place
    // lib/feature-flags.ts
    export function isNewFeatureEnabled(flag: boolean | undefined) {
      return flag === true;
    }
    ```
    
    **Why it's wrong:** Hard to find all usages during cleanup, easy to miss one.
    
    > See [SKILL.md](SKILL.md) for red flags, gotchas, and edge cases.
    
  • SKILL.md 11.3 KB
    ---
    name: api-flags-posthog-flags
    description: PostHog feature flags, rollouts, A/B testing. Use when implementing gradual rollouts, A/B tests, kill switches, remote configuration, beta features, or user targeting with PostHog.
    ---
    
    # Feature Flags with PostHog
    
    > **Quick Guide:** Use PostHog feature flags for gradual rollouts, A/B testing, and remote configuration. Client-side: `useFeatureFlagEnabled` hook. Server-side: `posthog-node` with local evaluation. Always pair `useFeatureFlagPayload` with `useFeatureFlagEnabled` for experiments. Handle the `undefined` loading state on every flag check.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST always pair `useFeatureFlagPayload` with `useFeatureFlagEnabled` or `useFeatureFlagVariantKey` for experiments - payload hooks don't send exposure events)**
    
    **(You MUST use the feature flags secure API key (phs\_\*) for server-side local evaluation - personal API keys are deprecated for this use)**
    
    **(You MUST handle the `undefined` state when flags are loading - never assume a flag is immediately available)**
    
    **(You MUST include flag owner and expiry date in flag metadata - flags without owners become orphaned debt)**
    
    **(You MUST wrap flag usage in a single function when used in multiple places - prevents orphaned flag code on cleanup)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** PostHog feature flags, useFeatureFlagEnabled, useFeatureFlagPayload, useFeatureFlagVariantKey, PostHogFeature, isFeatureEnabled, getFeatureFlag, gradual rollout, A/B test, experiment, multivariate flag
    
    **When to use:**
    
    - Gradual rollouts (deploy to 10% users, then 50%, then 100%)
    - A/B testing with experiments (measure impact of changes)
    - Kill switches (instantly disable features without deploy)
    - Remote configuration (change behavior without code changes)
    - Beta features opt-in (let users try new features)
    - User targeting (show features to specific cohorts)
    
    **When NOT to use:**
    
    - Simple on/off switches that never change (use environment variables)
    - Configuration that must be compile-time (use build flags)
    - Secrets or sensitive data (use secret management)
    - Features that should always be on (just ship the code)
    
    **Key patterns covered:**
    
    - Client-side flag evaluation with React hooks
    - Server-side local evaluation for performance
    - Boolean vs multivariate flags
    - Gradual rollouts with percentage targeting
    - A/B testing and experiments
    - Payloads for remote configuration
    - Local development overrides
    - Flag cleanup and lifecycle management
    
    ---
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Boolean flags, multivariate flags, PostHogFeature component, payloads, experiments, rollouts, lifecycle management
    - [examples/server-side.md](examples/server-side.md) - Server-side evaluation, local evaluation setup, distributed environments
    - [examples/development.md](examples/development.md) - Local overrides, bootstrapping, onFeatureFlags callback
    - [reference.md](reference.md) - Decision frameworks and anti-patterns
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    Feature flags decouple deployment from release. You can ship code to production but control who sees it and when. This enables:
    
    1. **Safe releases** - Roll out to 1% first, monitor, then expand
    2. **Fast rollback** - Toggle off instantly without deploying
    3. **Data-driven decisions** - A/B test to measure impact
    4. **Progressive delivery** - Beta users first, then everyone
    
    **Core principles:**
    
    - Flags are temporary - plan for cleanup from day one
    - Flags have owners - someone is responsible for each flag
    - Simple flags are better - percentage rollouts over complex conditions
    - Handle undefined - flags load asynchronously
    
    **When to use feature flags:**
    
    - Risky features that need gradual rollout
    - Features requiring A/B testing for validation
    - Features that may need instant rollback
    - Beta programs with user opt-in
    
    **When NOT to use feature flags:**
    
    - Every feature (creates maintenance burden)
    - Permanent configuration (use config files)
    - Features that are ready for 100% release
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Client-Side Boolean Flags
    
    Use `useFeatureFlagEnabled` for simple on/off features. Always handle the `undefined` loading state -- treating it as `false` causes a flash of wrong UI.
    
    ```typescript
    const isNewCheckout = useFeatureFlagEnabled(FLAG_NEW_CHECKOUT);
    
    if (isNewCheckout === undefined) return <Skeleton />;   // Loading
    if (isNewCheckout) return <NewCheckout />;               // Enabled
    return <LegacyCheckout />;                               // Disabled
    ```
    
    Store flag keys as named constants in `lib/feature-flags.ts` to prevent typos and enable cleanup-by-grep.
    
    See [examples/core.md](examples/core.md#pattern-1-client-side-boolean-flags) for full good/bad examples.
    
    ---
    
    ### Pattern 2: Multivariate Flags and Variants
    
    Use `useFeatureFlagVariantKey` for A/B tests with multiple variants. Define variant constants alongside the flag key. Switch on variants with a default fallback to `control`.
    
    ```typescript
    const variant = useFeatureFlagVariantKey(FLAG_PRICING_PAGE);
    if (variant === undefined) return <Skeleton />;
    
    switch (variant) {
      case VARIANT_SIMPLE: return <SimplePricing />;
      case VARIANT_DETAILED: return <DetailedPricing />;
      default: return <ControlPricing />;
    }
    ```
    
    See [examples/core.md](examples/core.md#pattern-2-multivariate-flags-and-variants) for full example.
    
    ---
    
    ### Pattern 3: PostHogFeature Component
    
    The `PostHogFeature` component provides automatic exposure tracking and built-in fallback handling with less boilerplate. Use `match={true}` for boolean flags or `match={VARIANT_KEY}` for specific variants.
    
    ```typescript
    <PostHogFeature flag={FLAG_BETA} match={true} fallback={<Legacy />}>
      <NewFeature />
    </PostHogFeature>
    ```
    
    See [examples/core.md](examples/core.md#pattern-3-posthogfeature-component) for boolean and variant examples.
    
    ---
    
    ### Pattern 4: Payloads for Remote Configuration
    
    Use `useFeatureFlagPayload` for dynamic JSON configuration. **Always pair with `useFeatureFlagEnabled`** -- the payload hook alone does NOT send exposure events, breaking experiment tracking.
    
    ```typescript
    const isEnabled = useFeatureFlagEnabled(FLAG_BANNER); // Sends exposure event
    const payload = useFeatureFlagPayload(FLAG_BANNER); // Gets config
    const config = payload ?? DEFAULT_BANNER_CONFIG;
    ```
    
    See [examples/core.md](examples/core.md#pattern-4-feature-flag-payloads-for-remote-configuration) for full good/bad examples.
    
    ---
    
    ### Pattern 5: Server-Side Flag Evaluation
    
    Use `posthog-node` with the Feature Flags Secure API Key (`phs_*`) for local evaluation. This reduces latency from ~500ms (network call) to ~10-50ms (local). The `personalApiKey` config option takes the `phs_*` key despite its legacy name.
    
    ```typescript
    export const posthog = new PostHog(process.env.POSTHOG_API_KEY!, {
      host: process.env.POSTHOG_HOST || "https://us.i.posthog.com",
      personalApiKey: process.env.POSTHOG_FEATURE_FLAGS_KEY, // phs_* key
      featureFlagsPollingInterval: POSTHOG_POLL_INTERVAL_MS, // default 30s
    });
    ```
    
    See [examples/server-side.md](examples/server-side.md) for API handler usage, local-only evaluation, and distributed/serverless environments.
    
    ---
    
    ### Pattern 6: Flag Lifecycle and Cleanup
    
    Every flag needs an owner, a creation date, and an expected removal date. Wrap flag checks in a single helper function so cleanup is a one-file change.
    
    ```typescript
    /**
     * Owner: @john-doe | Created: 2025-01-15 | Remove by: 2025-02-15
     */
    export const FLAG_NEW_CHECKOUT = "new-checkout-flow";
    
    export function isNewCheckoutEnabled(flag: boolean | undefined): boolean {
      return flag === true; // When removing: change to `return true;`
    }
    ```
    
    See [examples/core.md](examples/core.md#pattern-7-flag-cleanup-and-lifecycle-management) for full documentation patterns and stale flag detection.
    
    </patterns>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Using `useFeatureFlagPayload` alone for experiments (no exposure tracking)
    - Exposing Feature Flags Secure API key (`phs_*`) on client (security violation)
    - No loading state handling (causes UI flash)
    - Flags without owners or expiry dates (becomes permanent debt)
    
    **Medium Priority Issues:**
    
    - Magic string flag keys instead of constants (typos, hard to grep)
    - Complex targeting rules on high-traffic flags (performance hit)
    - Local evaluation in serverless/edge without external cache (cold start issues)
    - Not using PostHog toolbar for local testing (harder debugging)
    
    **Common Mistakes:**
    
    - Checking flag in multiple places instead of wrapper function
    - Not bootstrapping flags for SSR (content flash on hydration)
    - Running experiments without defined primary metric
    - Peeking at experiment results before completion
    - Rolling out to 100% without cleanup plan
    
    **Gotchas & Edge Cases:**
    
    - PostHog uses deterministic hashing - same user always gets same variant
    - Decreasing rollout percentage can remove users who were previously included
    - Local evaluation requires Feature Flags Secure API Key (`phs_*`) - personal API keys are deprecated
    - Flags load asynchronously - first render always has undefined
    - GeoIP targeting uses server IP by default in posthog-node v3+
    - Experiments need minimum 50 exposures per variant for results
    - Stale flag = 100% rollout + not evaluated in 30 days
    - `onFeatureFlags` callback receives three parameters: `flags`, `flagVariants`, `{ errorsLoading }` (third parameter)
    - External cache providers (Redis, KV) are experimental - Node.js/Python SDKs only
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST always pair `useFeatureFlagPayload` with `useFeatureFlagEnabled` or `useFeatureFlagVariantKey` for experiments - payload hooks don't send exposure events)**
    
    **(You MUST use the feature flags secure API key (phs\_\*) for server-side local evaluation - personal API keys are deprecated for this use)**
    
    **(You MUST handle the `undefined` state when flags are loading - never assume a flag is immediately available)**
    
    **(You MUST include flag owner and expiry date in flag metadata - flags without owners become orphaned debt)**
    
    **(You MUST wrap flag usage in a single function when used in multiple places - prevents orphaned flag code on cleanup)**
    
    **Failure to follow these rules will cause incorrect experiment results, security vulnerabilities, UI flashing, and technical debt.**
    
    </critical_reminders>
    
    ---
    
    ## Sources
    
    - [PostHog React Integration](https://posthog.com/docs/libraries/react)
    - [PostHog Feature Flags](https://posthog.com/docs/feature-flags)
    - [PostHog Feature Flag Best Practices](https://posthog.com/docs/feature-flags/best-practices)
    - [PostHog Server-Side Local Evaluation](https://posthog.com/docs/feature-flags/local-evaluation)
    - [PostHog Creating Feature Flags](https://posthog.com/docs/feature-flags/creating-feature-flags)
    - [PostHog How to Do a Phased Rollout](https://posthog.com/tutorials/phased-rollout)
    - [PostHog Feature Flag Testing](https://posthog.com/docs/feature-flags/testing)
    - [PostHog Experiments](https://posthog.com/ab-testing)
    - [PostHog Feature Flag Overrides](https://posthog.com/docs/toolbar/override-feature-flags)
    - [Don't Make These Feature Flag Mistakes](https://posthog.com/newsletter/feature-flag-mistakes)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related