Circle V2 API Docs
    Preparing search index...

    Module @repo/feature-flags

    @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)
    %%{init:{"theme":"dark"}}%% 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_config
    %%{init:{"theme":"default"}}%% 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_config
    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_config

    Pick 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.
    • Passing a registered key with a default whose type doesn't match the registered value is a type error.

    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.

    Modules

    launchdarkly/client
    launchdarkly/server
    react
    server
    static/client
    static/server