Fargo Flags Documentation
Complete guide to the enhanced feature flags toolkit built on Vercel's Flags SDK.
Fargo Flags Documentation
Complete guide to the enhanced feature flags toolkit built on Vercel's Flags SDK.
1. Overview
Fargo Flags is a streamlined toolkit built on top of Vercel's Flags SDK that adds enhanced developer experience, CLI tooling, and component registry distribution. It embraces the Flags SDK's "flags as code" principles while making them easier to adopt and scale.
Built on Solid Foundation
We leverage Vercel's Flags SDK core principles:
- Flags as code: Declarative definitions with consistent call sites
- Server-side resolution: Secure, performant flag evaluation
- Type safety: Full TypeScript support with runtime validation
- No vendor lock-in: Your flag logic stays in your codebase
Enhanced Developer Experience
Fargo Flags adds powerful tooling on top of this foundation:
- Interactive CLI wizard: Guided flag creation without manual boilerplate
- Automatic registry management: No need to manually maintain imports
- Component registry distribution: Install via shadcn/ui-style commands
- Enhanced React integration: Providers, hooks, and conditional components
- Testing utilities: Easy flag overrides for development and QA
- Consistency validation: Catch configuration drift in CI/CD
2. Installation
Core System
Install the core Fargo Flags system using the shadcn CLI:
📦 Core Installation Includes
- •
src/lib/flags/kit.ts
- Core types and defineFlag helper - •
src/lib/flags/runtime.ts
- Server-side resolver + client serialization - •
src/components/flags/flags-provider.tsx
- React context provider - •
src/lib/flags/registry.config.ts
- Starter registry with anchor tags - •
src/lib/flags/defs/.gitkeep
- Empty directory for flag definitions
Optional Components
Install additional components as needed:
Flag Component
Installs <Flag>
conal rendering component
Test Provider
Installs FlagsTestProvider
for testing
CLI Tools
🛠️ CLI Installation Includes
- •
scripts/create-flag.ts
- Interactive flag creation wizard - •
scripts/check-flags-registry.ts
- Consistency validation tool - • Instructions to add npm scripts to your
package.json
📋 Required Package.json Scripts
After installing the CLI tools, add these scripts to your package.json
:
{ "scripts": { "flags:new": "tsx scripts/create-flag.ts", "flags:check": "tsx scripts/check-flags-registry.ts" } }
💡 The shadcn CLI cannot automatically modify package.json, so this step is manual.
Manual Dependencies
If not using the registry, install dependencies manually:
Runtime Dependencies
Development Tools (for CLI)
Package.json Scripts
{ "scripts": { "flags:new": "tsx scripts/create-flag.ts", "flags:check": "tsx scripts/check-flags-registry.ts" } }
⚠️ Important Notes
- • The shadcn CLI cannot automatically update
package.json
scripts - • You must manually add the
flags:new
andflags:check
scripts - • CLI tools require
tsx
to run TypeScript files directly - • Prettier is optional but recommended for code formatting
3. Quick Start
1. Set up the core files
Copy the core system files into your project structure:
src/ ├── lib/flags/ │ ├── kit.ts # Core types │ ├── runtime.ts # Server resolver │ ├── registry.config.ts # Flag registry │ └── defs/ # Flag definitions ├── components/flags/ │ ├── flags-provider.tsx # React context │ ├── flag.tsx # Conditional component │ └── flags-test-provider.tsx # Testing utilities └── scripts/ ├── create-flag.ts # Flag wizard └── check-flags-registry.ts # Consistency checker
2. Integrate with your app
3. Install CLI Tools
This installs the flag creation wizard and consistency checker scripts.
📝 Manual Step Required
Add these scripts to your package.json
:
"scripts": { "flags:new": "tsx scripts/create-flag.ts", "flags:check": "tsx scripts/check-flags-registry.ts" }
4. Create your first flag
The interactive wizard will guide you through creating a properly typed flag with automatic registry updates.
5. Validate your setup
4. Defining Flags
Flag File Structure
Each flag is defined in its own file in src/lib/flags/defs/
:
Boolean Flag
// src/lib/flags/defs/my-feature.flag.ts import { z } from "zod"; import { defineFlag } from "../kit"; export const key = "my-awesome-feature" as const; export const schema = z.boolean(); export default defineFlag({ key, schema, description: "Enable my awesome new feature", defaultValue: false, client: { public: true }, // Expose to client async decide(ctx) { const user = await ctx.getUser?.(); return user?.plan === "premium"; }, });
Enum Flag
// src/lib/flags/defs/theme-mode.flag.ts import { z } from "zod"; import { defineFlag } from "../kit"; export const key = "theme-mode" as const; export const schema = z.enum(["light", "dark", "auto"]); export default defineFlag({ key, schema, description: "Application theme mode", defaultValue: "light", options: [ { value: "light", label: "Light Mode" }, { value: "dark", label: "Dark Mode" }, { value: "auto", label: "Auto (System)" } ], client: { public: true }, });
Server-Only Flag
// src/lib/flags/defs/ai-model.flag.ts import { z } from "zod"; import { defineFlag } from "../kit"; export const key = "ai-claims-model" as const; export const schema = z.enum([ "gpt-4o-mini", "gpt-4.5", "claude-3-sonnet" ]); export default defineFlag({ key, schema, description: "Which AI model to use for claims processing", defaultValue: "gpt-4o-mini", client: { public: false }, // Server-only async decide(ctx) { // Complex server-side logic const workspace = await ctx.getWorkspace?.(); return workspace?.plan === "enterprise" ? "gpt-4.5" : "gpt-4o-mini"; }, });
5. Understanding resolveAllFlags
resolveAllFlags
is the server-side engine that evaluates all your feature flags and returns their resolved values. It's the bridge between your flag definitions and your application.
How It Works
export async function resolveAllFlags(ctx?: FlagContext): Promise<Flags> { const keys = Object.keys(registry) as (keyof SchemaMap)[]; const entries = await Promise.all( keys.map(async (key) => { const def = registry[key]; // 🎯 This is where the magic happens: const raw = await Promise.resolve(def.decide?.(ctx) ?? def.defaultValue); const value = flagSchemas[key].parse(raw); // Zod validation return [key, value] as const; }) ); return Object.fromEntries(entries) as Flags; }
Step-by-Step Process
- Gets all flag keys from your registry
- Runs in parallel - all flags resolve simultaneously for performance
- For each flag:
- Calls the
decide()
function (if defined) with context - Falls back to
defaultValue
if nodecide()
function - Validates the result against the Zod schema
- Returns the final resolved value
- Calls the
Usage Patterns
Basic Usage (No Context)
// Simple flags that don't need user/workspace data const flags = await resolveAllFlags(); // All flags use their defaultValue or simple decide() logic
With Context (Recommended)
// Flags that need user/workspace information for decisions const flags = await resolveAllFlags({ getUser: async () => getCurrentUser(), getWorkspace: async () => getCurrentWorkspace(), });
In Next.js App Router (Primary Use Case)
// app/layout.tsx export default async function RootLayout({ children }) { // 🚀 This runs on every request const serverFlags = await resolveAllFlags({ getUser: async () => { const session = await getServerSession(); return session?.user || null; }, getWorkspace: async () => { const workspaceId = headers().get('x-workspace-id'); return workspaceId ? await getWorkspace(workspaceId) : null; }, }); const clientFlags = pickClientFlags(serverFlags); return ( <html> <body> <FlagsProvider flags={clientFlags}> {children} </FlagsProvider> </body> </html> ); }
The Complete Flow
Flag Definition → Resolution → Usage
// 1. Define a flag with decision logic export default defineFlag({ key: "enable-premium-features", schema: z.boolean(), defaultValue: false, client: { public: true }, async decide(ctx) { // 🎯 This function runs during resolveAllFlags() const user = await ctx.getUser?.(); const workspace = await ctx.getWorkspace?.(); return user?.plan === "premium" || workspace?.plan === "enterprise"; }, }); // 2. resolveAllFlags() calls the decide() function const serverFlags = await resolveAllFlags({ getUser: () => getCurrentUser(), getWorkspace: () => getCurrentWorkspace(), }); // Result: { "enable-premium-features": true } (if user has premium plan) // 3. pickClientFlags() filters for client-safe flags const clientFlags = pickClientFlags(serverFlags); // Result: { "enable-premium-features": true } (public flag, so included) // 4. Components use the resolved values function MyComponent() { const isPremium = useFlag("enable-premium-features"); // true return isPremium ? <PremiumFeatures /> : <UpgradePrompt />; }
Key Benefits
🛡️ Server-Side Resolution
- Security: Sensitive logic stays on the server
- Performance: Complex decisions don't slow down the client
- Consistency: Same flag values across the entire request
⚡ Parallel Execution
- All flags resolve simultaneously - not sequentially
- Optimal performance for multiple flags
- Efficient use of server resources
When to Use resolveAllFlags
✅ Use it for:
- App initialization (layout.tsx)
- API routes that need flag values
- Server actions that depend on flags
- Middleware for routing decisions
❌ Don't use it for:
- Client-side components (use useFlag instead)
- Static generation (flags are dynamic)
- Edge runtime with Node.js APIs in decide() functions
Advanced Patterns
Error Handling
const flags = await resolveAllFlags({ getUser: async () => { try { return await getCurrentUser(); } catch (error) { console.warn("User lookup failed, using defaults"); return null; // Flags will use defaultValue } }, });
Caching for Performance
import { cache } from "react"; // Cache expensive operations per request const getCachedUser = cache(async () => { return await expensiveUserLookup(); }); const flags = await resolveAllFlags({ getUser: getCachedUser, // Only runs once per request });
Conditional Context
const flags = await resolveAllFlags({ getUser: isAuthenticated ? getCurrentUser : undefined, getWorkspace: hasWorkspaceAccess ? getCurrentWorkspace : undefined, });
6. Using Flags
useFlag Hook
import { useFlag } from "@/components/flags/flags-provider"; function MyComponent() { const isEnabled = useFlag("my-awesome-feature"); const themeMode = useFlag("theme-mode"); return ( <div> {isEnabled && <NewFeature />} <div className={themeMode === "dark" ? "dark-theme" : "light-theme"}> Content </div> </div> ); }
Flag Component
import { Flag } from "@/components/flags/flag"; function MyComponent() { return ( <div> {/* Simple boolean check */} <Flag when="my-awesome-feature"> <NewFeature /> </Flag> {/* Negation */} <Flag when="my-awesome-feature" not={true}> <OldFeature /> </Flag> {/* Specific value check */} <Flag when="theme-mode" is="dark"> <DarkModeStyles /> </Flag> {/* With fallback */} <Flag when="loading-state" fallback={<Spinner />}> <Content /> </Flag> </div> ); }
Server-Side Usage
// In server components or API routes import { resolveAllFlags } from "@/lib/flags/runtime"; export async function GET() { const flags = await resolveAllFlags({ getUser: async () => getCurrentUser(), getWorkspace: async () => getCurrentWorkspace(), }); const aiModel = flags["ai-claims-model"]; // Use server-only flag value return Response.json({ model: aiModel }); }
7. Components
FlagsProvider
The main context provider that makes flag values available throughout your application. This component integrates with Next.js App Router to provide SSR-first flag resolution with client-side hydration.
Props
flags
:ClientFlags
- The client-safe flag values frompickClientFlags()
children
:React.ReactNode
- Your app components
Next.js App Router Integration
The recommended setup uses resolveAllFlags()
on the server and pickClientFlags()
to create a client-safe subset. This ensures optimal performance and security.
// app/layout.tsx (Server Component) import { ReactNode } from "react"; import { resolveAllFlags, pickClientFlags } from "@/lib/flags/runtime"; import { FlagsProvider } from "@/components/flags/flags-provider"; export default async function RootLayout({ children }: { children: ReactNode }) { // 1. Resolve ALL flags on the server (including server-only flags) const serverFlags = await resolveAllFlags({ // Optional context for flag decision logic getUser: async () => { // Your user fetching logic return await getCurrentUser(); }, getWorkspace: async () => { // Your workspace fetching logic return await getCurrentWorkspace(); }, }); // 2. Extract only client-safe flags (respects client.public setting) const clientFlags = pickClientFlags(serverFlags); return ( <html lang="en"> <body> <FlagsProvider flags={clientFlags}> {children} </FlagsProvider> </body> </html> ); }
Understanding resolveAllFlags()
This function runs on the server and resolves all flags, including their decision logic:
Key Features:
- Server-side execution: All
decide()
functions run on the server - Context support: Pass user/workspace data for personalized flags
- Parallel resolution: All flags are resolved concurrently for performance
- Schema validation: Flag values are validated against Zod schemas
- Default fallbacks: Uses
defaultValue
ifdecide()
is not provided
// Example flag with decision logic export default defineFlag({ key: "enable-premium-features", schema: z.boolean(), defaultValue: false, client: { public: true }, async decide(ctx) { // This runs on the server during resolveAllFlags() const user = await ctx.getUser?.(); const workspace = await ctx.getWorkspace?.(); // Complex server-side logic return user?.plan === "premium" || workspace?.plan === "enterprise"; }, }); // resolveAllFlags() will: // 1. Call the decide() function with your context // 2. Validate the result against the schema // 3. Return the resolved value
Understanding pickClientFlags()
This function creates a client-safe subset by filtering flags based on their client.public
setting:
Security Features:
- Public-only filtering: Only flags with
client: { public: true }
are included - Serialization support: Applies optional
serialize()
functions - Type safety: Returns properly typed
ClientFlags
object - Server-only protection: Sensitive flags never reach the client
// Example: Server flags vs Client flags const serverFlags = await resolveAllFlags(ctx); // serverFlags contains ALL flags: // { // "enable-premium-features": true, // public: true // "ai-model-selection": "gpt-4", // public: false (server-only) // "theme-mode": "dark", // public: true // "internal-debug-mode": true // public: false (server-only) // } const clientFlags = pickClientFlags(serverFlags); // clientFlags contains ONLY public flags: // { // "enable-premium-features": true, // "theme-mode": "dark" // } // Note: ai-model-selection and internal-debug-mode are excluded
Advanced Context Usage
// Advanced context with error handling and caching import { cache } from "react"; // Cache user lookup for the request const getCachedUser = cache(async () => { try { return await getCurrentUser(); } catch (error) { console.warn("Failed to fetch user for flags:", error); return null; // Graceful fallback } }); const getCachedWorkspace = cache(async () => { try { return await getCurrentWorkspace(); } catch (error) { console.warn("Failed to fetch workspace for flags:", error); return null; } }); export default async function RootLayout({ children }) { const serverFlags = await resolveAllFlags({ getUser: getCachedUser, getWorkspace: getCachedWorkspace, }); const clientFlags = pickClientFlags(serverFlags); return ( <html lang="en"> <body> <FlagsProvider flags={clientFlags}> {children} </FlagsProvider> </body> </html> ); }
Flag Serialization
Use the serialize
option to transform flag values before sending to the client:
// Flag definition with serialization export default defineFlag({ key: "user-permissions", schema: z.object({ canEdit: z.boolean(), canDelete: z.boolean(), roles: z.array(z.string()), userId: z.string(), }), defaultValue: { canEdit: false, canDelete: false, roles: [], userId: "" }, client: { public: true, // Remove sensitive userId before sending to client serialize: (permissions) => ({ canEdit: permissions.canEdit, canDelete: permissions.canDelete, roles: permissions.roles, // userId is excluded for security }) }, }); // pickClientFlags() will apply the serialize function automatically
Error Handling & Debugging
// Robust error handling in layout export default async function RootLayout({ children }) { try { const serverFlags = await resolveAllFlags({ getUser: async () => { try { return await getCurrentUser(); } catch (error) { // Log but don't fail - flags will use defaults console.warn("User fetch failed, using default flags:", error); return null; } }, }); const clientFlags = pickClientFlags(serverFlags); // Optional: Log flag resolution in development if (process.env.NODE_ENV === "development") { console.log("Resolved flags:", { server: Object.keys(serverFlags).length, client: Object.keys(clientFlags).length, flags: clientFlags, }); } return ( <html lang="en"> <body> <FlagsProvider flags={clientFlags}> {children} </FlagsProvider> </body> </html> ); } catch (error) { // Fallback to empty flags if resolution fails completely console.error("Flag resolution failed:", error); return ( <html lang="en"> <body> <FlagsProvider flags={{} as any}> {children} </FlagsProvider> </body> </html> ); } }
Performance Considerations
⚡ Optimization Tips:
- Cache context functions: Use React's
cache()
to avoid duplicate user/workspace fetches - Keep decide() functions fast: They run on every request - avoid expensive operations
- Minimize client flags: Only expose flags that components actually need
- Use server-only flags: Keep sensitive logic and data on the server
- Consider static flags: Flags without
decide()
functions are fastest
useFlag Hook
A React hook that returns the current value of a specific flag. Provides full TypeScript autocomplete and type safety.
Signature
function useFlag<K extends keyof ClientFlags>(key: K): ClientFlags[K]
Examples
import { useFlag } from "@/components/flags/flags-provider"; function FeatureComponent() { // Boolean flag const isNewDashboard = useFlag("enable-new-dashboard"); // Enum flag const themeMode = useFlag("theme-mode"); // String flag const apiEndpoint = useFlag("api-endpoint-version"); return ( <div> {isNewDashboard && <NewDashboard />} <div className={`theme-${themeMode}`}> <ApiClient endpoint={apiEndpoint} /> </div> </div> ); } // TypeScript will provide autocomplete for flag names and infer return types function TypeSafeExample() { const theme = useFlag("theme-mode"); // TypeScript knows this is "light" | "dark" | "auto" // This would cause a TypeScript error: // const invalid = useFlag("non-existent-flag"); return <div className={theme === "dark" ? "dark" : "light"}>Content</div>; }
Best Practices
- Use descriptive variable names that match the flag purpose
- Extract flag values at the top of your component for clarity
- Avoid calling
useFlag
conditionally or in loops - Consider memoizing expensive computations based on flag values
Flag Component
A declarative component for conditional rendering based on flag values. Provides a clean, readable way to show/hide content without cluttering your JSX with conditionals.
Props
when
:string
- The flag key to check (required)is
:any
- Render children when flag value equals thisnot
:any
- Render children when flag value does NOT equal thisfallback
:ReactNode
- Content to show when condition is falsechildren
:ReactNode
- Content to show when condition is true
Boolean Flags
import { Flag } from "@/components/flags/flag"; function Dashboard() { return ( <div> <h1>Dashboard</h1> {/* Show when flag is truthy */} <Flag when="enable-analytics"> <AnalyticsPanel /> </Flag> {/* Show when flag is falsy */} <Flag when="enable-analytics" not={true}> <div>Analytics coming soon!</div> </Flag> {/* With fallback content */} <Flag when="enable-premium-features" fallback={<UpgradePrompt />} > <PremiumFeatures /> </Flag> </div> ); }
Enum/String Flags
function ThemedApp() { return ( <div> {/* Specific value matching */} <Flag when="theme-mode" is="dark"> <DarkModeStyles /> </Flag> <Flag when="theme-mode" is="light"> <LightModeStyles /> </Flag> <Flag when="theme-mode" is="auto"> <SystemThemeDetector /> </Flag> {/* Multiple conditions */} <Flag when="user-plan" is="premium"> <PremiumBadge /> </Flag> <Flag when="user-plan" not="free"> <PaidFeatures /> </Flag> </div> ); }
Advanced Patterns
function AdvancedFlagUsage() { return ( <div> {/* Nested flags */} <Flag when="enable-new-ui"> <Flag when="enable-beta-features"> <BetaNewUI /> </Flag> <Flag when="enable-beta-features" not={true}> <StableNewUI /> </Flag> </Flag> {/* Complex fallback chains */} <Flag when="payment-provider" is="stripe" fallback={ <Flag when="payment-provider" is="paypal" fallback={<DefaultPaymentForm />} > <PayPalForm /> </Flag> } > <StripeForm /> </Flag> {/* Conditional styling */} <Flag when="high-contrast-mode"> <div className="high-contrast"> <Content /> </div> </Flag> </div> ); }
Performance Considerations
⚡ Performance Tips:
- Flag components are lightweight and don't cause re-renders when flag values change
- Prefer
<Flag>
over conditional JSX for better readability - Use
fallback
prop instead of separate<Flag not={true}>
when possible - Avoid deeply nested Flag components - consider using the hook for complex logic
FlagsTestProvider
A specialized provider for testing and development that allows you to override flag values. Perfect for unit tests, integration tests, and Storybook stories.
Props
overrides
:Partial<ClientFlags>
- Flag values to overridechildren
:React.ReactNode
- Components to test
Unit Testing
import { render, screen } from "@testing-library/react"; import { FlagsTestProvider } from "@/components/flags/flags-test-provider"; import Dashboard from "./Dashboard"; describe("Dashboard", () => { test("shows analytics when flag is enabled", () => { render( <FlagsTestProvider overrides={{ "enable-analytics": true }}> <Dashboard /> </FlagsTestProvider> ); expect(screen.getByText("Analytics Panel")).toBeInTheDocument(); }); test("hides analytics when flag is disabled", () => { render( <FlagsTestProvider overrides={{ "enable-analytics": false }}> <Dashboard /> </FlagsTestProvider> ); expect(screen.queryByText("Analytics Panel")).not.toBeInTheDocument(); }); test("handles multiple flag overrides", () => { render( <FlagsTestProvider overrides={{ "enable-analytics": true, "theme-mode": "dark", "user-plan": "premium" }} > <Dashboard /> </FlagsTestProvider> ); expect(screen.getByText("Analytics Panel")).toBeInTheDocument(); expect(screen.getByText("Premium Features")).toBeInTheDocument(); }); });
Storybook Integration
// Dashboard.stories.tsx import type { Meta, StoryObj } from "@storybook/react"; import { FlagsTestProvider } from "@/components/flags/flags-test-provider"; import Dashboard from "./Dashboard"; const meta: Meta<typeof Dashboard> = { title: "Components/Dashboard", component: Dashboard, }; export default meta; type Story = StoryObj<typeof Dashboard>; export const Default: Story = { decorators: [ (Story) => ( <FlagsTestProvider overrides={{}}> <Story /> </FlagsTestProvider> ), ], }; export const WithAnalytics: Story = { decorators: [ (Story) => ( <FlagsTestProvider overrides={{ "enable-analytics": true }}> <Story /> </FlagsTestProvider> ), ], }; export const DarkTheme: Story = { decorators: [ (Story) => ( <FlagsTestProvider overrides={{ "theme-mode": "dark", "enable-analytics": true }} > <Story /> </FlagsTestProvider> ), ], }; export const PremiumUser: Story = { decorators: [ (Story) => ( <FlagsTestProvider overrides={{ "user-plan": "premium", "enable-analytics": true, "enable-premium-features": true }} > <Story /> </FlagsTestProvider> ), ], };
Development & Debugging
// Create a debug component for development function FlagDebugger() { return ( <FlagsTestProvider overrides={{ "enable-new-dashboard": true, "theme-mode": "dark", "enable-beta-features": true, "user-plan": "premium" }} > <div style={{ border: "2px solid red", padding: "10px" }}> <h3>🚧 Development Mode - Flags Overridden</h3> <YourApp /> </div> </FlagsTestProvider> ); } // Use in development export default process.env.NODE_ENV === "development" ? FlagDebugger : YourApp;
Testing Best Practices
✅ Testing Guidelines:
- Test both enabled and disabled states for each flag
- Create separate test cases for different flag combinations
- Use descriptive test names that include the flag state
- Group related flag tests using
describe
blocks - Consider creating test utilities for common flag combinations
- Test edge cases like missing or invalid flag values
useFlags Hook
Returns all available client flags as an object. Useful for debugging, analytics, or when you need to work with multiple flags at once.
Usage
import { useFlags } from "@/components/flags/flags-provider"; function FlagDebugPanel() { const allFlags = useFlags(); return ( <div className="debug-panel"> <h3>Current Flag Values</h3> <pre>{JSON.stringify(allFlags, null, 2)}</pre> <div> <p>Total flags: {Object.keys(allFlags).length}</p> <p>Enabled features: { Object.values(allFlags).filter(Boolean).length }</p> </div> </div> ); } function ConditionalFeatures() { const flags = useFlags(); // Check multiple flags at once const hasAdvancedFeatures = flags["enable-analytics"] && flags["enable-premium-features"]; return ( <div> {hasAdvancedFeatures && <AdvancedDashboard />} </div> ); }
⚠️ When to use useFlags vs useFlag:
- Use
useFlag
for single flag values (most common) - Use
useFlags
for debugging, analytics, or complex multi-flag logic - Avoid
useFlags
in production components unless necessary
8. Testing
Unit Tests
import { render } from "@testing-library/react"; import { FlagsTestProvider } from "@/components/flags/flags-test-provider"; import MyComponent from "./MyComponent"; test("shows new feature when flag is enabled", () => { render( <FlagsTestProvider overrides={{ "my-feature": true }}> <MyComponent /> </FlagsTestProvider> ); expect(screen.getByText("New Feature")).toBeInTheDocument(); });
Storybook
// MyComponent.stories.tsx export const WithFeatureEnabled = { decorators: [ (Story) => ( <FlagsTestProvider overrides={{ "my-feature": true }}> <Story /> </FlagsTestProvider> ), ], }; export const WithFeatureDisabled = { decorators: [ (Story) => ( <FlagsTestProvider overrides={{ "my-feature": false }}> <Story /> </FlagsTestProvider> ), ], };
9. CLI Tools
Fargo Flags includes powerful CLI tools to streamline flag management. These tools are distributed via the shadcn registry and provide an interactive wizard for creating flags and a consistency checker for CI/CD pipelines.
Installation & Setup
1. Install CLI Tools
This installs scripts/create-flag.ts
and scripts/check-flags-registry.ts
2. Add Package.json Scripts
The shadcn CLI cannot modify package.json automatically, so add these scripts manually:
{ "scripts": { "flags:new": "tsx scripts/create-flag.ts", "flags:check": "tsx scripts/check-flags-registry.ts" } }
3. Install Dependencies
The CLI tools require these development dependencies:
flags:new - Interactive Flag Creation
The flag creation wizard guides you through defining new feature flags with proper TypeScript types, Zod schemas, and automatic registry updates.
Usage
Interactive Prompts
Flag Key
Enter a kebab-case identifier (e.g., enable-ai-assistant
)
✓ pagination-ui-location
✗ EnableAIAssistant
✗ enable_ai_assistant
Value Type
Choose between:
- • boolean - Simple on/off flags
- • string enum - Multiple predefined options
Client Exposure
Choose whether this flag should be available on the client-side:
- • Public: Available via
useFlag()
and<Flag>
- • Server-only: Only available in
resolveAllFlags()
Default Value
The fallback value when no decide()
function is provided or when it returns undefined.
What Gets Created
Automatic File Generation
- Flag Definition:
src/lib/flags/defs/your-flag.flag.ts
Complete flag definition with schema, defaults, and client settings - Registry Updates:
src/lib/flags/registry.config.ts
Automatic import, schema registration, and client key management - Code Formatting: Prettier formatting (if available)Clean, consistent code style
Example Workflow
$ pnpm flags:new ✔ Flag key (kebab-case) … enable-premium-features ✔ Value type › boolean ✔ Expose to client? … yes ✔ Default value … false ✔ Description (optional) … Enable premium features for paid users ✔ created src/lib/flags/defs/enable_premium_features.flag.ts ✔ updated src/lib/flags/registry.config.ts ✔ formatted src/lib/flags/defs/enable_premium_features.flag.ts ✔ formatted src/lib/flags/registry.config.ts
flags:check - Registry Consistency Validation
The consistency checker validates that your flag definitions and registry are in sync. This is essential for CI/CD pipelines to catch configuration drift.
Usage
pnpm flags:check
What It Validates
Registry Completeness
- • All flag files have registry entries
- • All registry entries have corresponding files
- • Schema definitions match registry keys
Client Flag Consistency
- • Public flags are in clientFlagKeys array
- • clientFlagKeys only contains public flags
- • No orphaned client keys
File Structure
- • All .flag.ts files export required 'key'
- • Flag keys match filename conventions
- • No duplicate flag keys
Import Integrity
- • All imports resolve correctly
- • No missing or broken imports
- • Registry anchor tags are intact
Success Output
✔ flags:check OK — 4 registered, 4 files, 3 client-exposed
Error Output
Defs present but missing in registry.config: - new-experimental-flag Public flags in files but missing from clientFlagKeys: - enable-ai-assistant-in-pdf-toolbar
CI/CD Integration
Add the consistency checker to your CI pipeline to catch flag configuration issues early.
GitHub Actions
# .github/workflows/ci.yml name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: pnpm install - run: pnpm flags:check # Validate flag consistency - run: pnpm build - run: pnpm test
Pre-commit Hook
# package.json { "lint-staged": { "src/lib/flags/**/*.ts": ["pnpm flags:check"] } }
Advanced Usage
Custom Flag Templates
You can modify scripts/create-flag.ts
to customize the generated flag templates:
// Add custom prompts { type: "text", name: "team", message: "Owning team" }, // Customize generated content const contents = `import { z } from "zod"; import { defineFlag } from "../kit"; /** * ${answers.description} * @owner ${answers.team} */ export const key = "${key}" as const; // ... rest of template`;
Batch Operations
For bulk flag management, you can extend the scripts:
// scripts/migrate-flags.ts - Custom migration script import { registry } from "../src/lib/flags/registry.config"; // Bulk update flag properties // Migrate deprecated flags // Generate flag usage reports
Fargo Flags includes powerful CLI tools to streamline flag creation and maintenance. These tools are distributed via the shadcn/ui-style component registry.
Installation
Install the CLI tools using the shadcn CLI:
npx shadcn@latest add https://flags.griffen.codes/r/flags-cli
📦 What Gets Installed
- •
scripts/create-flag.ts
- Interactive flag creation wizard - •
scripts/check-flags-registry.ts
- Consistency validation tool - • Instructions to add npm scripts to your
package.json
Setup Package Scripts
After installation, add these scripts to your package.json
:
{ "scripts": { "flags:new": "tsx scripts/create-flag.ts", "flags:check": "tsx scripts/check-flags-registry.ts" }, "devDependencies": { "tsx": "^4.20.5", "prompts": "^2.4.2", "fast-glob": "^3.3.3", "prettier": "^3.6.2" } }
Create Flag Wizard
Interactive wizard that creates a new flag file and updates the registry automatically.
pnpm flags:new
Wizard Flow
What the Wizard Does
- ✅ Creates flag file in
src/lib/flags/defs/
- ✅ Updates
registry.config.ts
with imports and entries - ✅ Adds to
clientFlagKeys
if public - ✅ Formats code with Prettier (if available)
- ✅ Validates flag key naming conventions
Consistency Checker
Validates that all flag definitions are properly registered and consistent. Essential for CI/CD pipelines and team development.
pnpm flags:check
What It Validates
- 🔍 Every
defs/*.flag.ts
file is imported inregistry.config.ts
- 🔍 All imports have corresponding entries in
flagSchemas
andregistry
- 🔍
flagSchemas
keys matchregistry
keys exactly - 🔍
clientFlagKeys
aligns with each flag'sclient.public
setting - 🔍 No orphaned registry entries without corresponding flag files
- 🔍 Flag key naming conventions are followed
Example Output
✔ flags:check OK — 4 registered, 4 files, 2 client-exposed # Or if issues are found: Defs present but missing in registry.config: - new-feature-flag Public flags in files but missing from clientFlagKeys: - enable-analytics
CI/CD Integration
Add the consistency checker to your CI pipeline:
# .github/workflows/ci.yml name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: pnpm install - run: pnpm flags:check # Validate flag consistency - run: pnpm test
Manual Flag Creation
You can also create flags manually without the wizard. The system supports both approaches:
1. Create Flag File
// src/lib/flags/defs/my-feature.flag.ts import { z } from "zod"; import { defineFlag } from "../kit"; export const key = "my-feature" as const; export const schema = z.boolean(); export default defineFlag({ key, schema, description: "My new feature", defaultValue: false, client: { public: true }, });
2. Update Registry Manually
// src/lib/flags/registry.config.ts // Add import import * as f_my_feature from "./defs/my_feature.flag"; // Add to flagSchemas export const flagSchemas = { "my-feature": f_my_feature.schema, // ... other flags } as const; // Add to registry export const registry = { "my-feature": f_my_feature.default, // ... other flags } as const; // Add to clientFlagKeys if public export const clientFlagKeys = [ "my-feature", // ... other public flags ] as const;
3. Validate Changes
pnpm flags:check
💡 Pro Tips
- • Use the wizard for speed and consistency
- • Run
flags:check
before committing changes - • Set up pre-commit hooks to validate flags automatically
- • Use descriptive flag keys that explain the feature
- • Keep flag descriptions up to date for team clarity
10. Architecture
How It Works
- Define flags in individual
*.flag.ts
files - Wizard updates
registry.config.ts
with static imports - Server resolves all flags during SSR with optional context
- Client receives safe subset via React provider
- Components use hooks or
<Flag>
wrapper
File Structure
src/ ├── lib/flags/ │ ├── kit.ts # Core types and defineFlag helper │ ├── runtime.ts # Server-side resolver + client serialization │ ├── registry.config.ts # Aggregator (checked in; wizard updates) │ └── defs/ # One file per flag │ ├── feature-a.flag.ts │ └── feature-b.flag.ts ├── components/flags/ │ ├── flags-provider.tsx # React context provider │ ├── flag.tsx # Conditional rendering component │ └── flags-test-provider.tsx # Testing utilities └── scripts/ ├── create-flag.ts # Flag scaffolding wizard └── check-flags-registry.ts # CI consistency checker
11. Best Practices
Flag Naming
- Use kebab-case for flag keys:
enable-new-dashboard
- Be descriptive:
show-premium-features
vspremium
- Include context:
checkout-flow-v2
vsv2
Security
- Keep sensitive flags server-only:
client: { public: false }
- Use
serialize
to sanitize public flag values - Never expose API keys or secrets in flag values
Performance
- Keep
decide()
functions fast - they run on every request - Use Next.js
cache()
for expensive flag decisions - Minimize the number of client-exposed flags
Testing
- Test both enabled and disabled states of features
- Use
FlagsTestProvider
for consistent test environments - Include flag states in your Storybook stories
12. Troubleshooting
Common Issues
Flag not found error
Error: Flag "my-flag" not found
Run pnpm flags:check
to ensure the flag is properly registered.
TypeScript errors
Property does not exist on type
Restart your TypeScript server after adding new flags.
Hydration mismatches
Server and client flag values differ
Ensure flag decisions are deterministic or use server-only flags.
13. API Reference
defineFlag()
defineFlag({ key: string; // Unique flag identifier schema: ZodSchema; // Zod schema for validation description?: string; // Human-readable description defaultValue: T; // Default value (must match schema) options?: Array<{ // For enum flags value: T; label?: string; }>; client?: { // Client exposure settings public: boolean; serialize?: (value: T) => any; }; decide?: (ctx: FlagContext) => T | Promise<T>; // Server-side decision logic })
useFlag()
function useFlag<K extends keyof ClientFlags>(key: K): ClientFlags[K]
Returns the current value of a client-exposed flag.
resolveAllFlags()
function resolveAllFlags(ctx?: FlagContext): Promise<Flags>
Resolves all flags on the server with optional context.
FlagContext
type FlagContext = { getUser?: () => Promise<{ id: string; plan?: string } | null>; getWorkspace?: () => Promise<{ id: string; plan?: string } | null>; }
Context object passed to flag decision functions.