March 06, 20267 min read

Building Fargo Flags: A Developer-First Feature Flag Toolkit
October 11, 2025 · 12 min read
TL;DR
- Built on Vercel’s Flags SDK with a thin DX layer.
- One-file-per-flag, type-safe, server-first with opt-in client side inclusion.
- shadcn/ui-style component registry and an interactive CLI wizard.
- Zero build step: checked-in registry with static imports.
- Try it:
npx shadcn@latest add https://flags.griffen.codes/r/flags-coreand.../flags-cli.
Feature flags have become an indespensible part of my development workflow in recent years. Specifically, I’ve been using Vercel’s Flags SDK on a multiple personal and professional projects and I think it’s great! 👏 But alas, I have fallen victim to my insufferable urge to automate & simplify my interaction with technology at every opportunity.
And so, as a result I built Fargo Flags: a streamlined toolkit that enhances the Flags SDK with a small suite of core components and CLI tools to assist managing projects with an ever-expanding number of features… and flags.
The Problem with Existing Solutions
Most feature flag services follow the same pattern:
- Store flag configurations in external dashboards
- Require API calls to resolve flag values
- Create vendor lock-in through proprietary SDKs
- Lack type safety and compile-time validation
Vercel’s Flags SDK took a different approach with “flags as code” – keeping flag logic in your codebase where it belongs. But while the architecture was solid, the developer experience had room for improvement:
- Manual boilerplate for each flag
- Registry management by hand
- No CLI tools for common workflows
- Limited React integration
The Fargo Flags Approach
Fargo Flags builds on the Flags SDK’s foundation while addressing these pain points:
🎯 One File Per Flag Architecture
Each feature flag lives in its own file with complete type safety:
// src/lib/flags/defs/enable-ai-assistant.flag.ts
import { z } from "zod";
import { defineFlag } from "../kit";
export const key = "enable-ai-assistant-in-pdf-toolbar" as const;
export const schema = z.boolean();
export default defineFlag({
key,
schema,
description: "Enable AI assistant in PDF toolbar",
defaultValue: false,
client: { public: true }, // Expose to client
async decide(ctx) {
const user = await ctx.getUser?.();
return user?.plan === "premium";
},
});🚀 Interactive CLI Wizard
Creating flags becomes effortless with the interactive wizard:
$ pnpm flags:new
✔ Flag key (kebab-case) … enable-premium-features
✔ Value type › boolean
✔ Expose to client? … yes
✔ Default value … false
✔ Description … Enable premium features for paid users
✔ created src/lib/flags/defs/enable-premium-features.flag.ts
✔ updated src/lib/flags/registry.config.tsThe wizard handles all the boilerplate:
- Creates the flag definition file
- Updates the registry with proper imports
- Manages client exposure settings
- Formats code with Prettier
Or run the consistency checker anytime:
pnpm flags:checkThis validates the registry completeness and client exposure, perfect for CI.
Architecture at a Glance
Flag Definitions (one file per flag)
│
▼
Checked-in Registry (registry.config.ts) ← auto-updated by CLI
│
▼
Server Resolution (resolveAllFlags)
│
▼
Client Subset (pickClientFlags) → <FlagsProvider>
│
├── useFlag('key')
└── <Flag when="key" />
🔒 Server-First Resolution with Client Hydration
Flags resolve on the server for security and performance, then hydrate client-safe values:
// app/layout.tsx
export default async function RootLayout({ children }) {
// Resolve ALL flags on server (including sensitive ones)
const serverFlags = await resolveAllFlags({
getUser: async () => getCurrentUser(),
getWorkspace: async () => getCurrentWorkspace(),
});
// Extract only client-safe flags
const clientFlags = pickClientFlags(serverFlags);
return (
<html>
<body>
<FlagsProvider flags={clientFlags}>
{children}
</FlagsProvider>
</body>
</html>
);
}⚡ Enhanced React Components
Clean, declarative flag usage in components:
import { useFlag } from "@/components/flags/flags-provider";
import { Flag } from "@/components/flags/flag";
function Dashboard() {
const isPremium = useFlag("enable-premium-features");
return (
<div>
<h1>Dashboard</h1>
{/* Hook-based usage */}
{isPremium && <PremiumFeatures />}
{/* Declarative component */}
<Flag when="enable-analytics">
<AnalyticsPanel />
</Flag>
{/* With fallback */}
<Flag
when="enable-premium-features"
fallback={<UpgradePrompt />}
>
<PremiumFeatures />
</Flag>
</div>
);
}
🧪 Testing Made Simple
Override flags easily in tests and Storybook:
import { FlagsTestProvider } from "@/components/flags/flags-test-provider";
// Unit tests
test("shows premium features when enabled", () => {
render(
<FlagsTestProvider overrides={{ "enable-premium-features": true }}>
<Dashboard />
</FlagsTestProvider>
);
expect(screen.getByText("Premium Features")).toBeInTheDocument();
});
// Storybook stories
export const PremiumUser = {
decorators: [
(Story) => (
<FlagsTestProvider
overrides={{
"enable-premium-features": true,
"theme-mode": "dark"
}}
>
<Story />
</FlagsTestProvider>
),
],
};
📦 Component Registry Distribution
Install via familiar shadcn/ui-style commands:
# Core system
npx shadcn@latest add https://flags.griffen.codes/r/flags-core
# Optional components
npx shadcn@latest add https://flags.griffen.codes/r/flags-flag
npx shadcn@latest add https://flags.griffen.codes/r/flags-test-provider
# CLI tools
npx shadcn@latest add https://flags.griffen.codes/r/flags-cli
🔍 CI/CD Integration
Validate flag consistency in your pipeline:
$ pnpm flags:check
✔ flags:check OK - 4 registered, 4 files, 3 client-exposed
# Or catch issues early:
Defs present but missing in registry.config:
- new-experimental-flag
Public flags in files but missing from clientFlagKeys:
- enable-ai-assistant-in-pdf-toolbar
Technical Architecture
The resolveAllFlags Engine
The heart of the system is resolveAllFlags() – a server-side engine that:
- Parallel Resolution: All flags resolve simultaneously for optimal performance
- Context Support: Pass user/workspace data for personalized decisions
- Schema Validation: Zod schemas ensure runtime type safety
- Graceful Fallbacks: Uses
defaultValuewhendecide()functions aren’t provided
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;
}
Security Model
- Server-only flags: Sensitive logic never reaches the client
- Client filtering:
pickClientFlags()respectsclient.publicsettings - Serialization: Optional
serialize()functions sanitize client values - Context isolation: Flag decisions run in secure server environment
Zero Build Step
Unlike code generation approaches, Fargo Flags uses static imports with a checked-in aggregator. The CLI wizard programmatically updates this file using special comment tags, which is a simple and robust way to manage updates to a source file.
// registry.config.ts - maintained by CLI wizard
// @fargo-flags:imports
import * as f_enable_ai_assistant from "./defs/enable_ai_assistant.flag";
import * as f_theme_mode from "./defs/theme_mode.flag";
// @fargo-flags:imports:end
export const registry = {
// @fargo-flags:registry
"enable-ai-assistant-in-pdf-toolbar": f_enable_ai_assistant.default,
"theme-mode": f_theme_mode.default,
// @fargo-flags:registry:end
} as const;
This “set and forget” approach provides several advantages:
- No build step required: Your app consumes the registry directly.
- Full TypeScript integration: Get immediate IDE autocomplete and refactoring support.
- Easy debugging: The registry is a plain, human-readable TypeScript file.
- Automated updates: The CLI wizard ensures new flags are generated correctly and automatically included downstream, including being exposed to the client if marked public.
Real-World Usage Patterns
Feature Rollouts
export default defineFlag({
key: "new-checkout-flow",
schema: z.boolean(),
defaultValue: false,
client: { public: true },
async decide(ctx) {
const user = await ctx.getUser?.();
// Gradual rollout based on user ID
const hash = hashUserId(user?.id);
return hash % 100 < 25; // 25% of users
},
});A/B Testing
export default defineFlag({
key: "pricing-page-variant",
schema: z.enum(["control", "variant-a", "variant-b"]),
defaultValue: "control",
client: { public: true },
async decide(ctx) {
const user = await ctx.getUser?.();
const hash = hashUserId(user?.id);
if (hash % 3 === 0) return "variant-a";
if (hash % 3 === 1) return "variant-b";
return "control";
},
});
Environment-Based Configuration
export default defineFlag({
key: "ai-model-selection",
schema: z.enum(["gpt-4o-mini", "gpt-4", "claude-3-sonnet"]),
defaultValue: "gpt-4o-mini",
client: { public: false }, // Server-only
async decide(ctx) {
const workspace = await ctx.getWorkspace?.();
if (process.env.NODE_ENV === "development") {
return "gpt-4o-mini"; // Cheaper for dev
}
return workspace?.plan === "enterprise"
? "claude-3-sonnet"
: "gpt-4";
},
});
Why This Approach Works
Developer Experience First
Every decision prioritizes developer productivity:
- Interactive CLI reduces cognitive load
- Type safety catches errors at compile time
- Consistent patterns across all flags
- Minimal boilerplate and ceremony
Production Ready
Built for real applications:
- Server-side resolution for security
- Parallel flag evaluation for performance
- CI/CD validation prevents configuration drift
- Comprehensive testing utilities
Getting Started
Try Fargo Flags in your Next.js project:
# Install core system
npx shadcn@latest add https://flags.griffen.codes/r/flags-core
# Install CLI tools
npx shadcn@latest add https://flags.griffen.codes/r/flags-cli
# Add package.json scripts
{
"scripts": {
"flags:new": "tsx scripts/create-flag.ts",
"flags:check": "tsx scripts/check-flags-registry.ts"
}
}
# Create your first flag
pnpm flags:new
What’s Next
Fargo Flags represents a new approach to feature flags – one that embraces the “flags as code” philosophy while providing the tooling to save developers time and provide
The project is open source and actively maintained. I’m excited to see how teams adopt it and what improvements the community suggests.
Try it out and let me know what you think! The full documentation and examples are available at flags.griffen.codes.
- Star the repo: https://github.com/gfargo/fargo-flags
- Try the live demo: https://flags.griffen.codes
- Install the core + CLI and create your first flag today
Fargo Flags is built on top of Vercel’s Flags SDK and follows their excellent architectural patterns. Special thanks to the Vercel team for their work 👏
Discussion
Have thoughts? Drop them in.
Comments are powered by Disqus. Sign in once, comment anywhere.


