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
Install globally
npx skills add tushaarmehtaa/tushar-skills --skill feature-flags -g -a codex -yInvoke
$feature-flags or /skillsYou can also describe the task naturally; runtimes may select the skill from its description.
Required access
Claude Code
Skills directory: ~/.claude/skills
Install globally
npx skills add tushaarmehtaa/tushar-skills --skill feature-flags -g -a claude-code -yInvoke
/feature-flagsYou can also describe the task naturally; runtimes may select the skill from its description.
Required access
Cursor
Skills directory: ~/.cursor/skills
Install globally
npx skills add tushaarmehtaa/tushar-skills --skill feature-flags -g -a cursor -yInvoke
/feature-flagsYou can also describe the task naturally; runtimes may select the skill from its description.
Required 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.mdWire 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
- Evaluating flags client-side when you need server-side behavior.
useFeatureFlagEnabledruns 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. - Missing
distinctIdin 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-jsinstalled + provider found? → PostHog client-side already set up. Add server-side SDK.posthog-nodealready 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:
- Feature Flags → New Feature Flag
- Key:
new-dashboard(matches what's in code) - Rollout: start at 0%, increment to 10%, 50%, 100%
- 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