back

feature-flags

Implement PostHog feature flags across Next.js server and client code with local overrides. Use when gating features, staging rollouts, or running experiments.

Category
ai
Package
feature-flags/SKILL.md
License
MIT
Author
@tushaarmehtaa
Tags
feature-flagsposthogrolloutab-testingexperiments

Install

Swipe for more runtimes.

Codex

Skills directory: ~/.codex/skills

available to install

Install globally

$npx skills add tushaarmehtaa/tushar-skills --skill feature-flags -g -a codex -y

Invoke

$feature-flags or /skills

You can also describe the task naturally; runtimes may select the skill from its description.

Required access

project filesterminal commandsnetwork access

local coding agent required

This skill requires project files, terminal commands, and network access. Uploading it to a chat app does not provide equivalent execution.

ChatGPT Skills

This workflow needs a local coding environment or capabilities that a chat-only Skills upload does not provide.

Why local agent required →

Instructions

Source: SKILL.md

Wire PostHog feature flags end-to-end — server-side evaluation, client-side hook, and local dev overrides. Reads the project first, extends the existing PostHog setup if present.

Two things that silently break PostHog flags

  1. Evaluating flags client-side when you need server-side behavior. useFeatureFlagEnabled runs in the browser after hydration. If you're using a flag to show/hide a page section, that section flashes on then disappears. For anything above the fold or that affects routing, evaluate the flag on the server.
  2. Missing distinctId in server-side calls. PostHog's server SDK requires a distinct user identifier to evaluate flags. Using a placeholder like "anonymous" means every unauthenticated user gets the same flag variant — not a rollout, just a constant.

Phase 1: Detect the Project

cat package.json | grep -E "posthog"
grep -r "posthog" src/ app/ --include="*.ts" --include="*.tsx" -l 2>/dev/null | head -5
  • posthog-js installed + provider found? → PostHog client-side already set up. Add server-side SDK.
  • posthog-node already installed? → extend it, don't reinstall.
  • Nothing? → wire up PostHog from scratch with the add-analytics skill first, then return here.

Phase 2: Install Server SDK

npm install posthog-node

Add to .env.example (already set if the add-analytics skill was run):

NEXT_PUBLIC_POSTHOG_KEY=
POSTHOG_API_KEY=   # same as POSTHOG_KEY — use for server-side calls

Phase 3: Server-Side Flag Client

Create a singleton for server-side evaluation:

// lib/posthog-server.ts
import { PostHog } from 'posthog-node';

let client: PostHog | null = null;

export function getPostHogServer(): PostHog {
  if (!client) {
    client = new PostHog(process.env.POSTHOG_API_KEY!, {
      host: 'https://us.i.posthog.com',
      flushAt: 1,      // flush immediately in serverless
      flushInterval: 0,
    });
  }
  return client;
}

export async function isFeatureEnabled(
  flagKey: string,
  distinctId: string,
  groups?: Record<string, string>
): Promise<boolean> {
  const ph = getPostHogServer();
  const result = await ph.isFeatureEnabled(flagKey, distinctId, { groups });
  return result ?? false;
}

export async function getFeatureFlagVariant(
  flagKey: string,
  distinctId: string
): Promise<string | boolean | undefined> {
  const ph = getPostHogServer();
  return ph.getFeatureFlag(flagKey, distinctId);
}

Phase 4: Server Component Usage

Evaluate flags in Server Components and route handlers — no client-side flicker:

// app/dashboard/page.tsx (Server Component)
import { isFeatureEnabled } from '@/lib/posthog-server';
import { getAuthUser } from '@/lib/auth';

export default async function DashboardPage() {
  const user = await getAuthUser();

  const showNewDashboard = user
    ? await isFeatureEnabled('new-dashboard', user.id)
    : false;

  return showNewDashboard ? <NewDashboard /> : <OldDashboard />;
}
// app/api/generate/route.ts (Route Handler)
import { isFeatureEnabled } from '@/lib/posthog-server';

export async function POST(req: Request) {
  const user = await getAuthUser(req);
  if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });

  const hasStreamingAccess = await isFeatureEnabled('streaming-responses', user.id);

  if (hasStreamingAccess) {
    return handleStreaming(req, user);
  }
  return handleNonStreaming(req, user);
}

Phase 5: Client-Side Usage

For flags that change UI after user interaction (not on initial render):

// components/feature-flagged-button.tsx
'use client';
import { useFeatureFlagEnabled } from 'posthog-js/react';

export function NewFeatureButton() {
  const isEnabled = useFeatureFlagEnabled('new-export-feature');

  // useFeatureFlagEnabled returns undefined while loading
  if (isEnabled === undefined) return null;
  if (!isEnabled) return null;

  return <button>Export (New)</button>;
}

Never use the client hook for above-the-fold content. The hook returns undefined on first render, which causes layout shift. Use the server-side approach for anything visible on page load.

Phase 6: Local Dev Overrides

Add a dev-only flag override mechanism so you can test both variants locally without touching PostHog:

// lib/posthog-server.ts (add to existing file)
export async function isFeatureEnabled(
  flagKey: string,
  distinctId: string,
  groups?: Record<string, string>
): Promise<boolean> {
  // Local override for development
  if (process.env.NODE_ENV === 'development') {
    const override = process.env[`FLAG_${flagKey.toUpperCase().replace(/-/g, '_')}`];
    if (override === 'true') return true;
    if (override === 'false') return false;
  }

  const ph = getPostHogServer();
  const result = await ph.isFeatureEnabled(flagKey, distinctId, { groups });
  return result ?? false;
}

In .env.local:

FLAG_NEW_DASHBOARD=true    # force-enable new-dashboard flag locally
FLAG_STREAMING_RESPONSES=false  # force-disable

Phase 7: Creating Flags in PostHog

Tell the user exactly what to set up. In PostHog dashboard:

  1. Feature Flags → New Feature Flag
  2. Key: new-dashboard (matches what's in code)
  3. Rollout: start at 0%, increment to 10%, 50%, 100%
  4. Conditions: can target by user properties (plan = 'pro'), cohorts, or percentage

For A/B tests: use "Multiple Variants" instead of boolean. Then use getFeatureFlagVariant() and return 'control' or 'test'.

Verify

[ ] posthog-node installed
[ ] POSTHOG_API_KEY in .env.example
[ ] Server-side evaluation uses real user ID as distinctId — not "anonymous"
[ ] Server Components use isFeatureEnabled() — not the client hook
[ ] Client hook used only for post-hydration interactions, not initial render
[ ] Local override via FLAG_* env vars works in development
[ ] Flags created in PostHog dashboard with matching key names
[ ] Test: set FLAG_[KEY]=false in .env.local — confirm feature is hidden
[ ] Test: set FLAG_[KEY]=true in .env.local — confirm feature is shown
[ ] PostHog flag set to 0% rollout first — expand after confirming it works