Valcraven Docs
Features

Feature Flags

A feature flag system with a three-tier transport — Cloudflare Flagship on Workers, an OpenFeature fallback, and local env/JSON flags for dev — plus built-in maintenance mode.

Feature Flags

Valcraven includes a feature flag system in lib/flags.ts. You evaluate flags with a small typed API (getFlag, getStringFlag, getNumberFlag) and the module picks the best available backend automatically. In production on Cloudflare it uses Cloudflare Flagship; in local dev it reads plain env vars or a flags.json file, so you can use flags immediately with no Cloudflare account.

Flagship is currently in private beta. The Workers binding is real but the [[flagship]] block in wrangler.toml ships commented out until you have a Flagship app_id. Until then, evaluation falls through to the local tier — which works everywhere — so nothing breaks.

The three transport tiers

lib/flags.ts tries three backends in order and falls to the next on failure (same pattern as lib/email.ts and lib/db.ts):

TierEnvironmentHow it works
1Cloudflare WorkersThe env.FLAGS Flagship binding — edge evaluation, zero config
2Docker / VPSThe OpenFeature SDK over HTTP (@openfeature/server-sdk + @cloudflare/flagship), needs FLAGSHIP_APP_ID + CLOUDFLARE_ACCOUNT_ID + CF_API_TOKEN
3Local devFLAG_* env vars or a flags.json file — no Cloudflare access needed

Tier 2's packages are optional. If they aren't installed, or its env vars are missing, evaluation simply falls through to tier 3.

Evaluating a flag

import { getFlag, getStringFlag, getNumberFlag, invalidateFlagCache } from "@/lib/flags";

// Boolean — is this feature on?
const enabled = await getFlag("new-dashboard", false, { userId, plan });

// String — which variant? (A/B testing)
const variant = await getStringFlag("checkout-variant", "control", { userId });

// Number — a tunable parameter
const limit = await getNumberFlag("max-upload-mb", 10, { plan });

// Reset cached local flags (useful between tests)
invalidateFlagCache();

The second argument is the default returned when the flag doesn't exist or evaluation fails, so a flag call is always safe.

The optional third argument is a FlagContext — user attributes passed to Flagship's targeting rules:

interface FlagContext {
  userId?: string;   // per-user targeting and percentage rollouts
  plan?: string;     // plan-based gating ("free" vs "pro")
  email?: string;    // email-based targeting
  [key: string]: string | undefined;  // custom attributes
}

Local development

Option 1 — environment variables

Set FLAG_<UPPERCASE_HYPHENATED_KEY> in .env:

FLAG_MAINTENANCE_MODE=true
FLAG_NEW_DASHBOARD=true
FLAG_MAX_UPLOAD_MB=50
FLAG_CHECKOUT_VARIANT=new-design

The key is lowercased and de-underscored: FLAG_MY_FEATUREmy-feature. Values are parsed — "true"/"false" become booleans, numeric strings become numbers, everything else stays a string.

Option 2 — flags.json

Create flags.json in the project root:

{
  "maintenance-mode": false,
  "new-dashboard": true,
  "max-upload-mb": 25,
  "checkout-variant": "control"
}

FLAG_* env vars take precedence over flags.json.

Reading flags in the browser

For React components, fetch the /api/flags endpoint rather than calling lib/flags.ts directly (it's server-only):

const res = await fetch("/api/flags");
const { flags } = await res.json();
// e.g. { "maintenance-mode": false, "new-dashboard": true }

The endpoint evaluates flags with the signed-in user's context (userId, plan, email); unauthenticated requests get global defaults.

The endpoint has an explicit list of flags in app/api/flags/route.ts — it does not auto-discover them. When you add a flag you want on the client, add a line there too.

Maintenance mode

A built-in flag demonstrates the system end to end. Toggle it with:

FLAG_MAINTENANCE_MODE=true

When it's on, middleware.ts rewrites all public routes to /maintenance. These paths stay accessible so you can still operate the app:

  • /api/* — API endpoints
  • /admin/* — the admin panel
  • /_next/*, /favicon* — static assets
  • /maintenance — the maintenance page itself

Middleware runs in the Edge runtime, so it reads process.env.FLAG_MAINTENANCE_MODE directly instead of calling lib/flags.ts (which uses require/fs and is Node.js-only). For full Flagship evaluation, use getFlag() in API routes and server components.

Adding a new flag

  1. (Production) Create it in Flagship — Cloudflare dashboard → Compute → Flagship → your app → Create flag. Set the key, type, default, and any targeting rules.
  2. Use it in code — call the matching getter with a sensible default:
    const enabled = await getFlag("your-flag-key", false, context);
  3. Expose it to the client (if needed) — add a line to app/api/flags/route.ts.
  4. Override it locally — add FLAG_YOUR_FLAG_KEY=... to .env or an entry in flags.json.

Turning on Flagship in production

The binding is declared (commented out) in wrangler.toml:

# [[flagship]]
# binding = "FLAGS"
# app_id = "<YOUR_FLAGSHIP_APP_ID>"

Once you have an app_id, uncomment the block, set it, and regenerate types with npx wrangler types. No code changes are needed — tier 1 activates automatically.

Key files

FilePurpose
lib/flags.tsCore module — three-tier transport and all getters
app/api/flags/route.tsClient-facing flag evaluation endpoint
app/maintenance/page.tsxMaintenance mode landing page
middleware.tsMaintenance mode rewrite
wrangler.tomlFlagship binding declaration (commented until you have an app_id)
flags.jsonOptional local flag overrides

On this page