@repo/feature-flags provides feature flag primitives for both server and browser code. It ships two provider implementations -- LaunchDarkly and an in-memory static map -- and leaves provider selection up to the consumer (no env-var magic).
On the browser, the ./react entry exports a FeatureFlagProvider plus useFeatureFlag / useFeatureFlags hooks that read flag values directly from LaunchDarkly's React SDK, so flag updates propagate in real time.
| Import | Resolves to | Description |
|---|---|---|
@repo/feature-flags |
src/index.ts |
FlagContext / FlagName / FeatureFlagRegistry types, BaseFeatureFlagProvider, BaseBrowserFeatureFlagClient, mergeFlagContext |
@repo/feature-flags/react |
src/react.tsx |
FeatureFlagProvider, useFeatureFlags, useFeatureFlag |
@repo/feature-flags/launchdarkly/server |
src/launchdarkly/server.ts |
LaunchDarklyFeatureFlagProvider (Node SDK) |
@repo/feature-flags/launchdarkly/client |
src/launchdarkly/client.ts |
LaunchDarklyFeatureFlagClient (browser config holder) |
@repo/feature-flags/static/server |
src/static/server.ts |
StaticFeatureFlagProvider (in-memory server provider) |
@repo/feature-flags/static/client |
src/static/client.ts |
StaticFeatureFlagClient (in-memory browser client) |
graph TD
feature_flags["@repo/feature-flags"]
errors["@repo/errors"]
logger["@repo/logger"]
typescript_config["@repo/typescript-config"]
vitest_config["@repo/vitest-config"]
feature_flags --> errors
feature_flags --> logger
feature_flags -.-> typescript_config
feature_flags -.-> vitest_configPick a provider explicitly -- the consumer owns which provider to instantiate and when.
import { LaunchDarklyFeatureFlagProvider } from "@repo/feature-flags/launchdarkly/server";
const featureFlags = new LaunchDarklyFeatureFlagProvider(process.env.LAUNCHDARKLY_SDK_KEY!);
await featureFlags.initialize();
const enabled = await featureFlags.isEnabled("my-feature", {
userId: "user-1",
email: "alice@example.com",
entityId: "org-1",
});
const limit = await featureFlags.getVariation("upload-limit", 10, {
userId: "user-1",
email: "alice@example.com",
entityId: "org-1",
});
identify(context) sets a default evaluation context for the current async chain (via AsyncLocalStorage), so subsequent isEnabled / getVariation calls can be invoked without an explicit context and still resolve against the identified user. The typical pattern is to call identify once per request -- for example, in a tRPC context factory after the user has been authenticated:
const featureFlags = getServerFeatureFlagProvider();
await ensureServerFeatureFlagsInitialized();
if (user) {
await featureFlags.identify({
userId: user.user_id,
email: user.email ?? undefined,
entityId: String(user.entity_id),
});
}
return { user, featureFlags /* ...other context fields */ };
Downstream isEnabled / getVariation calls within the same async chain will resolve against this user without needing to pass context explicitly.
Construct a client, pass it to FeatureFlagProvider, and use the hooks anywhere beneath it. Flag values update in real time via LaunchDarkly's React SDK.
import { LaunchDarklyFeatureFlagClient } from "@repo/feature-flags/launchdarkly/client";
import { FeatureFlagProvider, useFeatureFlag, useFeatureFlags } from "@repo/feature-flags/react";
const client = new LaunchDarklyFeatureFlagClient({
clientSideID: process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_SIDE_ID!,
});
function App({ children }: { children: React.ReactNode }) {
return <FeatureFlagProvider client={client}>{children}</FeatureFlagProvider>;
}
function MyComponent() {
const showNewNav = useFeatureFlag("new-nav");
const limit = useFeatureFlag("upload-limit", 10);
const featureFlags = useFeatureFlags();
// featureFlags.identify(...), featureFlags.getVariation(...), etc.
return (
<div>
<p>{showNewNav ? "new nav" : "old nav"}</p>
<p>Upload limit is {limit}</p>
</div>
);
}
useFeatureFlag(key) returns a boolean. Pass a default to get a typed non-boolean variation: useFeatureFlag("upload-limit", 10) returns number.
useFeatureFlags() returns a small accessor with isEnabled / getVariation / identify; use it for imperative calls like identify inside effects.
LaunchDarklyFeatureFlagClient starts out in anonymous mode. Once your user is authenticated, call identify so flag targeting rules can be evaluated against them. Destructure identify from useFeatureFlags() -- its identity is stable across flag updates, so it is safe to list in effect deps:
"use client";
import { useEffect } from "react";
import { useFeatureFlags } from "@repo/feature-flags/react";
import { useCurrentUser } from "./user.store";
export function FeatureFlagIdentity() {
const user = useCurrentUser();
const { identify } = useFeatureFlags();
useEffect(() => {
void identify({
userId: user.user_id,
email: user.email ?? undefined,
entityId: String(user.entity_id),
});
}, [identify, user.user_id, user.email, user.entity_id]);
return null;
}
Render <FeatureFlagIdentity /> under both FeatureFlagProvider and whatever provides the authenticated user. identify is a no-op when the context is structurally unchanged, so it is safe to call on every render.
By default, useFeatureFlag / useFeatureFlags accept any string key and return values typed as the default you pass in. To get autocomplete and typed return values, augment the FeatureFlagRegistry interface once per app in a .d.ts file:
import "@repo/feature-flags";
declare module "@repo/feature-flags" {
interface FeatureFlagRegistry {
enableCircleV2: boolean;
"upload-limit": number;
}
}
With the registry populated:
useFeatureFlag autocompletes the registered keys.useFeatureFlag("upload-limit", 10) returns number without an explicit type parameter.The registry is intentionally empty by default, so the package works without any registration -- flag keys simply fall back to string and return types fall back to the default you pass.
Swap in the static implementations to control flags deterministically without LaunchDarkly.
import { StaticFeatureFlagProvider } from "@repo/feature-flags/static/server";
import { StaticFeatureFlagClient } from "@repo/feature-flags/static/client";
const provider = new StaticFeatureFlagProvider({ "my-feature": true });
await provider.initialize();
await provider.isEnabled("my-feature"); // true
const client = new StaticFeatureFlagClient({ "my-feature": true });
client.isEnabled("my-feature"); // true
StaticFeatureFlagClient can also be passed to FeatureFlagProvider to render React tests without LaunchDarkly.
| Script | Description |
|---|---|
lint |
Runs Biome check on the package. |
test |
Runs Vitest with coverage. |
test:watch |
Runs Vitest in watch mode. |
check-types |
Runs tsc --noEmit to typecheck the package. |