Skip to content

sathergate/flagpost

Repository files navigation

flagpost

Feature flags for Next.js. One file, full control.

npm version license

Zero dependencies. Type-safe. Works with Server Components, Client Components, and Middleware.


Install

npm install flagpost

Quick Start

1. Define your flags

// 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",
  }),
});

2. Protect your UI (Client)

// 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>
  );
}

3. Check server-side

// 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>
  );
}

API Reference

Core

createFlagpost(config)

Creates a flagpost instance for evaluating feature flags.

import { createFlagpost } from "flagpost";

const fp = createFlagpost({
  flags: {
    myFlag: { defaultValue: true },
  },
  context: async () => ({ userId: getCurrentUserId() }),
});

fp.evaluate(name, context?)

Evaluate a single flag. Returns the resolved value.

const variant = fp.evaluate("heroVariant", { userId: "user-123" });

fp.evaluateAll(context?)

Evaluate all flags at once. Returns a typed record.

const all = fp.evaluateAll({ userId: "user-123" });
// { darkMode: false, heroVariant: "experiment", maxItems: 10 }

fp.isEnabled(name, context?)

Shorthand for boolean flags. Returns true only if the flag evaluates to true.

if (fp.isEnabled("darkMode")) {
  applyDarkTheme();
}

React (flagpost/react)

<FlagpostProvider>

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)

<Flag>

Conditionally renders children based on a boolean flag.

<Flag name="newCheckout" fallback={<OldCheckout />} loading={<Spinner />}>
  <NewCheckout />
</Flag>

<FlagSwitch>

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 />}
/>

useFlag(name)

Returns the value and status of a single flag.

const { value, isEnabled, isLoading } = useFlag("darkMode");

useFlags()

Returns all evaluated flags and loading state.

const { flags, isLoading } = useFlags();

useFlagpost()

Returns the raw flagpost instance.

const fp = useFlagpost();

Server / Next.js (flagpost/next)

flag(flagpost, name, context?)

Evaluate a single flag server-side. Resolves context automatically if not provided.

const darkMode = await flag(fp, "darkMode");

flags(flagpost, context?)

Evaluate all flags server-side.

const allFlags = await flags(fp);

createFlagMiddleware(flagpost, contextResolver, options?)

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";

Targeting & Rollouts

Percentage Rollout

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" }); // deterministic

User Targeting

Target 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" } },
      ],
    },
  },
});

Combined Rules

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.


TypeScript

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"

License

MIT

Part of sathergate-toolkit

This package is part of the sathergate-toolkit — an agent-native infrastructure toolkit for Next.js. All packages work independently or together.

See Also

  • 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)

About

Feature flags for Next.js. One file, full control.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors