soilmass

feature-flags

0
0
# Install this skill:
npx skills add soilmass/vibe-coding-plugin --skill "feature-flags"

Install specific skill from multi-skill repository

# Description

>

# SKILL.md


name: feature-flags
description: >
Feature flags with Vercel Edge Config, LaunchDarkly SDK, A/B testing with PostHog, gradual rollouts
allowed-tools: Read, Grep, Glob


Feature Flags

Purpose

Feature flag management for Next.js 15 with Vercel Edge Config for zero-latency reads,
LaunchDarkly server SDK for complex targeting, and PostHog integration for A/B testing.
Enables gradual rollouts and safe feature deployment.

When to Use

  • Implementing feature toggles for gradual rollout
  • A/B testing with variant assignment
  • Route-level feature gating in middleware
  • Percentage-based or user-segment targeting
  • Managing feature lifecycle (create β†’ test β†’ rollout β†’ cleanup)

When NOT to Use

  • Environment-specific config β†’ env-validation
  • Auth-based access control β†’ auth
  • Per-tenant configuration β†’ multi-tenancy
  • Runtime app settings UI β†’ custom admin panel

Pattern

Vercel Edge Config (zero-latency flags)

// src/lib/feature-flags.ts
import "server-only";
import { get } from "@vercel/edge-config";
import { z } from "zod";

const FlagSchema = z.object({
  newDashboard: z.boolean().default(false),
  checkoutVersion: z.enum(["v1", "v2", "v3"]).default("v1"),
  maxUploadSize: z.number().default(10),
});

type Flags = z.infer<typeof FlagSchema>;

export async function getFlag<K extends keyof Flags>(key: K): Promise<Flags[K]> {
  const value = await get(key);
  const parsed = FlagSchema.shape[key].safeParse(value);
  return parsed.success ? (parsed.data as Flags[K]) : FlagSchema.shape[key].parse(undefined);
}

export async function getAllFlags(): Promise<Flags> {
  const raw = await get("flags");
  return FlagSchema.parse(raw ?? {});
}

Server Component conditional rendering

// src/app/dashboard/page.tsx
import { getFlag } from "@/lib/feature-flags";

export default async function DashboardPage() {
  const useNewDashboard = await getFlag("newDashboard");

  if (useNewDashboard) {
    return <NewDashboard />;
  }
  return <LegacyDashboard />;
}

Middleware route gating

// src/middleware.ts
import { get } from "@vercel/edge-config";
import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith("/beta")) {
    const betaEnabled = await get("betaEnabled");
    if (!betaEnabled) {
      return NextResponse.redirect(new URL("/", request.url));
    }
  }
  return NextResponse.next();
}

LaunchDarkly server SDK

// src/lib/launchdarkly.ts
import "server-only";
import * as ld from "@launchdarkly/node-server-sdk";

let client: ld.LDClient;

export async function getLDClient() {
  if (!client) {
    client = ld.init(process.env.LAUNCHDARKLY_SDK_KEY!);
    await client.waitForInitialization({ timeout: 5 });
  }
  return client;
}

export async function getVariation<T>(
  flagKey: string,
  userId: string,
  defaultValue: T
): Promise<T> {
  const ldClient = await getLDClient();
  return ldClient.variation(flagKey, { key: userId }, defaultValue);
}

A/B test variant with PostHog

// src/lib/ab-test.ts
import "server-only";
import { cookies } from "next/headers";

export async function getVariant(
  experimentId: string,
  variants: string[]
): Promise<string> {
  const cookieStore = await cookies();
  const existing = cookieStore.get(`exp_${experimentId}`)?.value;
  if (existing && variants.includes(existing)) return existing;

  // Deterministic assignment based on visitor ID
  const visitorId = cookieStore.get("visitor_id")?.value ?? crypto.randomUUID();
  const hash = await hashString(`${experimentId}:${visitorId}`);
  const index = hash % variants.length;
  return variants[index];
}

async function hashString(input: string): Promise<number> {
  const encoder = new TextEncoder();
  const data = encoder.encode(input);
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  return new DataView(hashBuffer).getUint32(0);
}

Gradual rollout (percentage-based)

export async function isEnabledForUser(
  flagKey: string,
  userId: string,
  percentage: number
): Promise<boolean> {
  const hash = await hashString(`${flagKey}:${userId}`);
  return (hash % 100) < percentage;
}

Flag cleanup checklist

When removing a flag:
1. Set flag to 100% enabled for all users
2. Wait one release cycle to confirm stability
3. Remove all conditional code paths (keep only the enabled path)
4. Remove the flag from Edge Config / LaunchDarkly
5. Remove the flag from the Zod schema
6. Update tests to remove flag-dependent branches

Anti-pattern

Feature flags in client components

Reading flags in Client Components leaks flag state to the browser. Always read flags
in Server Components or middleware, then pass the resolved value as props.

No flag cleanup process

Stale flags accumulate as tech debt. Track flag creation dates and set cleanup
reminders. Each flag should have an owner and expiration.

Boolean-only flags

Use string enums for multi-variant experiments ("v1" | "v2" | "v3") instead of
boolean flags that limit you to on/off.

Common Mistakes

  • Calling get() in Client Components β€” Edge Config is server-only
  • Not awaiting cookies() in Next.js 15 β€” it returns a Promise
  • Inconsistent variant assignment β€” use deterministic hashing, not Math.random()
  • Missing default values β€” always provide fallbacks for when flag service is down
  • Testing only the enabled path β€” test both flag states

Checklist

  • [ ] Flag definitions use Zod schema with defaults
  • [ ] Flags read in Server Components, not Client Components
  • [ ] Middleware handles route-level feature gating
  • [ ] A/B test variants use deterministic assignment
  • [ ] Flag cleanup process documented
  • [ ] Tests cover both flag states

Composes With

  • nextjs-middleware β€” route-level feature gating
  • analytics β€” A/B test result tracking with PostHog
  • react-server-components β€” conditional rendering based on flags
  • testing β€” test both flag states
  • edge-computing β€” Edge Config for zero-latency reads

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.