Flagkeeper is a type-safe TypeScript feature flag library for Node.js and React. Define environment-aware defaults in an application-owned registry, resolve flags on the server, and pass typed snapshots to React.
bun add flagkeeperFlagkeeper has no runtime dependency beyond environment access during resolution.
| Runtime | Compatibility |
|---|---|
| Node.js | Uses the default process.env lookup. |
| Bun | Uses the default process.env lookup. |
| Cloudflare Workers | Pass the Worker env object in the resolve options. |
| Vercel Edge Runtime, Deno Deploy, and other edge runtimes | Pass an env object in the resolve options. |
| Browser/client React | FlagProvider, Flag, and useFlag consume an already-resolved snapshot, so they do not need environment access. |
Define the flags owned by your application:
import { defineFlags } from "flagkeeper";
export const flags = defineFlags({
NEW_CHECKOUT: {
description: "Enable the new checkout experience.",
defaults: { development: true, staging: true, production: false },
},
DASHBOARD_REDIRECT: {
description: "Redirect users to the new dashboard.",
defaults: { development: true, staging: true, production: false },
},
});Each flag requires a description plus development and production defaults.
Additional environment defaults, such as staging, are optional.
Flagkeeper exports the public types used by the registry and resolved snapshots:
import type {
EnvSource,
FlagDefinition,
FlagDefaults,
FlagEnvironment,
FlagKey,
FlagRegistry,
FlagSnapshot,
ResolveOptions,
} from "flagkeeper";Use FlagKey and FlagSnapshot with your registry type when you want helpers
to stay in sync with the flags you declared:
import type { FlagKey, FlagSnapshot } from "flagkeeper";
import { flags } from "./flags";
type AppFlagKey = FlagKey<typeof flags>;
type AppFlagSnapshot = FlagSnapshot<typeof flags>;FlagDefaults requires development and production values and allows extra
environment keys. EnvSource is the env-object shape accepted by
resolveFlag, resolveAllFlags, and resolveEnvironment.
import { flags } from "./flags";
import { resolveAllFlags, resolveFlag } from "flagkeeper";
const enabled = resolveFlag(flags, "NEW_CHECKOUT");
const snapshot = resolveAllFlags(flags);Unknown keys are rejected by TypeScript when using a typed registry.
After wrangler types generates Env from your Worker bindings, pass the
Worker env object to resolution:
import { flags } from "./flags";
import { resolveAllFlags } from "flagkeeper";
export default {
async fetch(request, env): Promise<Response> {
const snapshot = resolveAllFlags(flags, { env });
const pathname = new URL(request.url).pathname;
return Response.json({ pathname, flags: snapshot });
},
} satisfies ExportedHandler<Env>;Create typed React bindings once from your application's registry, then resolve on the server and pass its snapshot to the provider:
import { flags } from "./flags";
import { resolveAllFlags } from "flagkeeper";
import { createFlagContext } from "flagkeeper/react";
export const { FlagProvider } = createFlagContext<typeof flags>();
<FlagProvider flags={resolveAllFlags(flags)}>
{children}
</FlagProvider>;createFlagContext also returns Flag and useFlag for client components:
import { Flag, useFlag } from "./flag-context";
const enabled = useFlag("NEW_CHECKOUT");
<Flag flag="NEW_CHECKOUT">Enabled content</Flag>;If you are new to React, wrap hooks and JSX in components:
import type { ReactNode } from "react";
import { Flag, FlagProvider, useFlag } from "./flag-context";
import { flags } from "./flags";
import { resolveAllFlags } from "flagkeeper";
export function AppFlagsProvider({ children }: { readonly children: ReactNode }) {
return <FlagProvider flags={resolveAllFlags(flags)}>{children}</FlagProvider>;
}
export function CheckoutStatus() {
const enabled = useFlag("NEW_CHECKOUT");
return (
<section>
<p>{enabled ? "New checkout is enabled." : "New checkout is disabled."}</p>
<Flag flag="NEW_CHECKOUT">Enabled content</Flag>
</section>
);
}If the provider is absent or the key is not present in the snapshot, client reads return disabled by default.
Flag resolution reads NODE_ENV only:
- exactly
developmentuses thedevelopmentdefault - a value matching an additional declared default, such as
staging, uses that default - every other value, including unset and
test, uses theproductiondefault
For example, add a dedicated staging default when it should differ from production:
defaults: { development: true, staging: true, production: false }Use local environment variables to force a flag on or off while developing
without changing the registry defaults. The variable name is FLAG_ plus the
exact flag key:
FLAG_NEW_CHECKOUT=false bun devFor repeated local work, put overrides in your app's ignored local env file,
such as .env.local:
FLAG_NEW_CHECKOUT=true
FLAG_DASHBOARD_REDIRECT=falseKeep local override files out of git and remove overrides when validating
environment defaults. Override values are case-insensitive true or false.
Invalid override values fall back to the current environment default and warn
once.