ShipVeryFastShipVeryFast
Documentation

Feature flags

ShipVeryFast ships a small, dependency-aware feature-flag system. Flags live in a single JSON file, are read and written through typed helpers in libs/featureFlags.ts, and surface in the UI through a React provider and a ready-made admin toggle panel. This page is for developers who want to gate functionality on or off without a redeploy.

The FeatureFlag shape

Every flag is described by the FeatureFlag interface in libs/featureFlags.ts. A flag is an object with three required fields, and the whole store is a map of flag keys keyed under a top-level flags property.

// libs/featureFlags.ts
export interface FeatureFlag {
  enabled: boolean;        // is the feature on?
  description: string;     // human-readable label shown in the admin UI
  dependencies: string[];  // keys of other flags this one needs
}

export interface FeatureFlags {
  flags: {
    [key: string]: FeatureFlag;
  };
}

The dependencies array is the heart of the system: it lists the keys of other flags that must be enabled for this one to be valid. For example, stripe depends on auth, so you cannot have payments switched on while authentication is off.

The JSON store

The single source of truth on the server is feature-flags.json at the project root. libs/featureFlags.ts resolves it with path.join(process.cwd(), 'feature-flags.json')and reads/writes it with Node's fs module, so this is server-side only. The shipped file looks like this:

// feature-flags.json
{
  "flags": {
    "blog": {
      "enabled": true,
      "description": "Blog functionality with rich text editor and SEO optimization",
      "dependencies": []
    },
    "teams": {
      "enabled": false,
      "description": "Team collaboration features for multi-user accounts",
      "dependencies": ["auth"]
    },
    "auth": {
      "enabled": true,
      "description": "Authentication and user management",
      "dependencies": []
    },
    "stripe": {
      "enabled": true,
      "description": "Stripe payment processing and subscription management",
      "dependencies": ["auth"]
    },
    "email": {
      "enabled": true,
      "description": "Email notifications and transactional emails",
      "dependencies": ["auth"]
    },
    "darkMode": {
      "enabled": true,
      "description": "Dark mode theme support",
      "dependencies": []
    }
  }
}

Reads go through getFeatureFlags(), which parses the file and falls back to an empty store ({ flags: {} }) if the file is missing or unparseable. Writes go through saveFeatureFlags(), which validates dependencies first and then writes the JSON back with two-space indentation.

Server helpers

libs/featureFlags.ts exports a focused set of server functions. Use these in Server Components, route handlers, and scripts, never in the browser, since they touch the filesystem.

FunctionSignatureWhat it does
getFeatureFlags() => FeatureFlagsReads and parses feature-flags.json; returns an empty store on error.
isFeatureEnabled(featureKey: string) => booleanReturns true only if the flag exists and is enabled.
enableFeature(featureKey: string) => booleanTurns a flag on and enables all of its dependencies, then saves.
disableFeature(featureKey: string) => booleanTurns a flag off and disables anything that depends on it, then saves.
validateDependencies(flags: FeatureFlags) => voidThrows if any enabled flag has an unmet dependency.
saveFeatureFlags(flags: FeatureFlags) => booleanValidates then writes the store to disk; returns false on failure.

The two you reach for most often are checking and flipping a flag:

import { isFeatureEnabled, enableFeature } from "@/libs/featureFlags";

// Gate a Server Component or route handler
if (isFeatureEnabled("blog")) {
  // render the blog
}

// Flip a flag on (also enables its dependencies, then persists to JSON)
const ok = enableFeature("teams"); // also enables "auth"
if (!ok) {
  // flag key did not exist, or the write failed
}

Dependency validation

The dependency graph is enforced on every write. There are three behaviours worth understanding:

Enabling cascades to dependencies

enableFeature(featureKey) sets the flag's enabled to true and walks its dependencies array, switching each existing dependency on as well. Enabling stripe therefore also enables auth.

Disabling cascades to dependents

disableFeature(featureKey) sets the flag off, then scans every other flag and disables any whose dependencies include this key. Disabling auth will therefore also disable teams, stripe, and email, since all three depend on it.

Validation guards the whole store

Before any save, validateDependencies() checks every enabled flag against its dependencies and throws a descriptive error if one is missing or disabled:

// thrown when an enabled flag has an unmet dependency
Error: Feature flag dependencies not satisfied: teams requires auth

Because flags persist to a file on disk, the server helpers work great in local development and on long-lived Node hosts, but writes do not survive on ephemeral or read-only serverless filesystems. Treat feature-flags.json as committed defaults, and reach for an external store if you need durable runtime toggles in production.

Client usage

On the client, flags are exposed through FeatureFlagsProvider in components/providers/FeatureFlagsProvider.tsx. It is already mounted for the whole app in app/providers.tsx, so the context is available everywhere.

The provider exposes a useFeatureFlags hook (plural) that returns { flags, isEnabled, updateFlag, loading, error }. Use isEnabled to check a flag from any client component:

"use client";

import { useFeatureFlags } from "@/components/providers/FeatureFlagsProvider";

export function NewBanner() {
  const { isEnabled, loading } = useFeatureFlags();

  if (loading) return null;
  if (!isEnabled("blog")) return null;

  return <div>Read the blog</div>;
}

For the common "show this only when a feature is on" case, prefer the FeatureGate component in components/ui/FeatureGate.tsx. It takes a feature key, renders its children when the flag is enabled, and an optional fallback otherwise (rendering nothing while the provider is still loading):

"use client";

import FeatureGate from "@/components/ui/FeatureGate";

export function Pricing() {
  return (
    <FeatureGate
      feature="stripe"
      fallback={<p>Payments are coming soon</p>}
    >
      <CheckoutButton />
    </FeatureGate>
  );
}

The provider currently bootstraps from an in-memory set of flags and resolves it after a short delay, and updateFlag mutates local state for instant UI feedback. For durable, cross-session state the server source of truth remains feature-flags.json via the helpers and the API route below, wire the provider to GET /api/feature-flags if you want the client to read live values.

The API route and admin manager

app/api/feature-flags/route.ts exposes the store over HTTP:

MethodBehaviourErrors
GETReturns the current FeatureFlags via getFeatureFlags().500, failed to retrieve.
POSTValidates the body with a Zod schema, runs validateDependencies(), then persists with saveFeatureFlags() and echoes the saved flags.400, invalid format or unmet dependency; 500, save failed.

The request body is validated against a Zod schema that mirrors the FeatureFlag shape (enabled boolean, description string, dependencies string array), so malformed payloads are rejected before they ever touch disk.

The matching UI is FeatureFlagsManager in components/ui/admin/FeatureFlagsManager.tsx, mounted on the admin dashboard at app/admin/page.tsx. It reads from useFeatureFlags(), renders each flag with its description and dependency chips, and provides a toggle per flag that calls updateFlag and surfaces a success or error message.

Adding a new flag

Adding a flag is a two-file change: declare it in the JSON store, then gate the relevant code.

1. Add the entry

Add a new key under flags in feature-flags.json. List any prerequisite flags in dependencies so the cascade logic and validator know about them.

// feature-flags.json
"referrals": {
  "enabled": false,
  "description": "Referral program with shareable invite links",
  "dependencies": ["auth"]
}

2. Gate the feature

On the server, branch on isFeatureEnabled("referrals"). On the client, wrap UI in <FeatureGate feature="referrals"> or read isEnabled("referrals") from useFeatureFlags(). See components/examples/FeatureFlagExample.tsx for a worked example that gates the blog, teams, and darkMode flags.

"use client";

import FeatureGate from "@/components/ui/FeatureGate";

export function ReferralsCard() {
  return (
    <FeatureGate feature="referrals">
      <InviteFriendsPanel />
    </FeatureGate>
  );
}

Once the flag exists in the JSON store, it will automatically appear in the admin manager, complete with its description and dependency chips, ready to toggle.

Next steps