Feature flags for Next.js. One file, full control.
Zero dependencies. Type-safe. Works with Server Components, Client Components, and Middleware.
npm install flagpost// lib/flags.ts
import { createFlagpost } from "flagpost";
export const fp = createFlagpost({
flags: {
darkMode: {
defaultValue: false,
description: "Enable dark mode across the app",
},
heroVariant: {
defaultValue: "control" as const,
description: "A/B test for the hero section",
rules: [{ value: "experiment" as const, percentage: 50 }],
},
maxItems: {
defaultValue: 10,
description: "Maximum items per page",
rules: [{ value: 50, match: { plan: "pro" } }],
},
},
context: async () => ({
// Resolve user context however you like
userId: "anonymous",
}),
});// app/page.tsx
"use client";
import { FlagpostProvider, Flag, FlagSwitch } from "flagpost/react";
import { fp } from "@/lib/flags";
export default function App() {
return (
<FlagpostProvider flagpost={fp}>
<Flag name="darkMode" fallback={<LightTheme />}>
<DarkTheme />
</Flag>
<FlagSwitch
name="heroVariant"
cases={{
control: <HeroA />,
experiment: <HeroB />,
}}
/>
</FlagpostProvider>
);
}// app/dashboard/page.tsx (Server Component)
import { flag, flags } from "flagpost/next";
import { fp } from "@/lib/flags";
export default async function Dashboard() {
const darkMode = await flag(fp, "darkMode");
const allFlags = await flags(fp);
return (
<div className={darkMode ? "dark" : ""}>
<p>Max items: {allFlags.maxItems}</p>
</div>
);
}Creates a flagpost instance for evaluating feature flags.
import { createFlagpost } from "flagpost";
const fp = createFlagpost({
flags: {
myFlag: { defaultValue: true },
},
context: async () => ({ userId: getCurrentUserId() }),
});Evaluate a single flag. Returns the resolved value.
const variant = fp.evaluate("heroVariant", { userId: "user-123" });Evaluate all flags at once. Returns a typed record.
const all = fp.evaluateAll({ userId: "user-123" });
// { darkMode: false, heroVariant: "experiment", maxItems: 10 }Shorthand for boolean flags. Returns true only if the flag evaluates to true.
if (fp.isEnabled("darkMode")) {
applyDarkTheme();
}Wraps your app and evaluates all flags on mount.
<FlagpostProvider
flagpost={fp}
context={async () => ({ userId: user.id })}
>
{children}
</FlagpostProvider>| Prop | Type | Description |
|---|---|---|
flagpost |
Flagpost |
The flagpost instance |
context |
() => FlagContext | Promise<FlagContext> |
Optional context resolver (overrides config) |
Conditionally renders children based on a boolean flag.
<Flag name="newCheckout" fallback={<OldCheckout />} loading={<Spinner />}>
<NewCheckout />
</Flag>Renders a component based on the evaluated value of a multi-variant flag.
<FlagSwitch
name="pricingPage"
cases={{
control: <PricingA />,
variantB: <PricingB />,
variantC: <PricingC />,
}}
fallback={<PricingA />}
/>Returns the value and status of a single flag.
const { value, isEnabled, isLoading } = useFlag("darkMode");Returns all evaluated flags and loading state.
const { flags, isLoading } = useFlags();Returns the raw flagpost instance.
const fp = useFlagpost();Evaluate a single flag server-side. Resolves context automatically if not provided.
const darkMode = await flag(fp, "darkMode");Evaluate all flags server-side.
const allFlags = await flags(fp);Creates a Next.js middleware that evaluates all flags and injects them as request headers.
// middleware.ts
import { createFlagMiddleware } from "flagpost/next";
import { fp } from "@/lib/flags";
const withFlags = createFlagMiddleware(fp, (req) => ({
userId: req.cookies.get("userId")?.value ?? "anonymous",
country: req.geo?.country ?? "US",
}));
export function middleware(req) {
return withFlags(req);
}Flags are set as headers with the x-flag- prefix. CamelCase names are converted to kebab-case:
| Flag Name | Header |
|---|---|
darkMode |
x-flag-dark-mode |
heroVariant |
x-flag-hero-variant |
Read them in Server Components:
import { headers } from "next/headers";
const hdrs = await headers();
const darkMode = hdrs.get("x-flag-dark-mode") === "true";Roll out a flag to a percentage of users. Requires userId in context for deterministic bucketing.
const fp = createFlagpost({
flags: {
newDashboard: {
defaultValue: false,
rules: [{ value: true, percentage: 25 }], // 25% of users
},
},
});
fp.isEnabled("newDashboard", { userId: "user-42" }); // deterministicTarget specific user attributes with match. All keys must match.
const fp = createFlagpost({
flags: {
betaFeature: {
defaultValue: false,
rules: [
{ value: true, match: { plan: "enterprise" } },
{ value: true, match: { email: "tester@example.com" } },
],
},
},
});Use match and percentage together. Both conditions must be satisfied.
rules: [
// 50% of enterprise users
{ value: true, match: { plan: "enterprise" }, percentage: 50 },
];Rules are evaluated in order. The first matching rule wins. If no rules match, defaultValue is used.
Flag types are fully inferred from your definitions.
const fp = createFlagpost({
flags: {
darkMode: { defaultValue: false },
tier: { defaultValue: "free" as const },
maxItems: { defaultValue: 10 },
},
});
// Type-safe evaluation
const dark: boolean = fp.evaluate("darkMode");
const tier: "free" = fp.evaluate("tier");
const max: number = fp.evaluate("maxItems");
// Type error: "nonexistent" is not a valid flag name
fp.evaluate("nonexistent");Use the helper types for advanced use cases:
import type { ExtractFlags, ExtractFlagNames } from "flagpost";
type MyFlags = ExtractFlags<typeof fp.definitions>;
// { darkMode: boolean; tier: "free"; maxItems: number }
type MyFlagNames = ExtractFlagNames<typeof fp.definitions>;
// "darkMode" | "tier" | "maxItems"MIT
This package is part of the sathergate-toolkit — an agent-native infrastructure toolkit for Next.js. All packages work independently or together.
- shutterbox — Image processing pipeline (
npm i shutterbox) - ratelimit-next — Rate limiting with sliding window & token bucket (
npm i ratelimit-next) - notifykit — Unified notifications via Twilio, Resend, SNS (
npm i notifykit) - croncall — Serverless-native cron job scheduling (
npm i croncall) - vaultbox — AES-256-GCM encrypted secrets management (
npm i vaultbox) - searchcraft — Full-text search with BM25 scoring (
npm i searchcraft)