npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mergedapp/feature-flags

v0.1.2

Published

Type-safe client SDK for Merged feature flags with ES256 JWT verification

Readme

@mergedapp/feature-flags

Warning This SDK is not ready for public use yet. The package is public, but you will not be able to finish setting up feature flags until the main app is available. We will do a proper launch soon.

Type-safe, generic feature flag client SDK with ES256 JWT verification, code generation, and React hooks.

Additional integrations now ship on dedicated subpaths:

  • @mergedapp/feature-flags/react
  • @mergedapp/feature-flags/openfeature/web
  • @mergedapp/feature-flags/openfeature/server
  • @mergedapp/feature-flags/nestjs

The SDK reads already-evaluated flag values for a specific organization and environment. Authoring concepts such as stable codeKeys, environment overrides, fallback values, targeting groups, percentage rollouts, and IP, country, region, or attribute targeting are configured in the dashboard and resolved by the API before values reach the client.

Installation

npm install @mergedapp/feature-flags

If you want OpenFeature interoperability or NestJS helpers, install the matching peer packages too:

npm install @mergedapp/feature-flags @openfeature/core @openfeature/web-sdk @openfeature/server-sdk
npm install @mergedapp/feature-flags @nestjs/common @nestjs/core rxjs

Quick Start

1. Generate Typed Flags

Run the CLI to fetch your flag definitions and generate a type-safe TypeScript file:

npx merged-ff generate --api-url=<coming_soon> --client-key=lk_pub_your_key_here --organization-id=org_123

This creates ./src/generated/feature-flags.ts containing a createClient factory, typed hooks, and type definitions.

2. Create the Client

import { createClient } from "./generated/feature-flags"

const client = createClient({
  apiUrl: "<coming_soon>",
  clientKey: "lk_pub_your_key_here",
  organizationId: "org_123",
  environmentId: "env_prod",
  evaluationContext: {
    attributes: {
      user: {
        plan: { code: "pro" },
        role: "admin",
      },
      cart: {
        items: [{ sku: "sku_123" }],
      },
    },
  },
})

await client.initialize()

// Fully typed -- flag code keys are autocompleted, return types are inferred
if (client.isEnabled("enableDarkMode")) {
  enableDarkMode()
}

const retries = client.getValue("maxRetries") // number | undefined

// Compile error -- "typo" is not a valid flag code key
client.isEnabled("typo") // TS Error: Argument of type '"typo"' is not assignable

The generated file uses stable codeKey values as the SDK-facing identifiers. You can rename a flag's display name in the dashboard without breaking typed client lookups, but changing the codeKey is a breaking change for generated code.

Using a Config File

Create a featureflags.config.json in your project root. featureflags.config.js, .mjs, and .cjs are also supported:

{
  "apiUrl": "<coming_soon>",
  "clientKey": "lk_pub_your_key_here",
  "organizationId": "org_123",
  "outputPath": "./src/generated/feature-flags.ts"
}

Then run without arguments:

npx merged-ff generate

Environment variables FEATURE_FLAG_API_URL, FEATURE_FLAG_CLIENT_KEY, and FEATURE_FLAG_ORGANIZATION_ID are also supported.

Code Generation

The merged-ff generate CLI produces a TypeScript file with these exports:

FLAGS

A constant object mapping stable code keys to stable UUIDs:

export const FLAGS = {
  enableDarkMode: "550e8400-e29b-41d4-a716-446655440000",
  maxUploadSize: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
} as const

FlagValues

An interface mapping each flag code key to its TypeScript type:

export interface FlagValues {
  enableDarkMode: boolean
  maxUploadSize: number
}

createClient(config)

Creates a MergedFeatureFlags<FlagValues> instance with flagIds pre-configured from the generated FLAGS mapping. Provide the API origin plus the organization and environment scope used for evaluation:

import { createClient } from "./generated/feature-flags"

const client = createClient({
  apiUrl: "<coming_soon>",
  clientKey: "lk_pub_your_key_here",
  organizationId: "org_123",
  environmentId: "env_prod",
})

await client.initialize()

client.isEnabled("enableDarkMode") // boolean -- autocompleted
client.getValue("maxUploadSize") // number | undefined -- inferred
client.isEnabled("nonExistent") // Compile error

The client receives the final evaluated value for the configured organization and environment. If your dashboard configuration uses environment overrides, targeting groups, and percentage rollouts, the SDK sees the resolved result rather than the raw authoring definition.

If the active targeting rules or rollout bucketing depend on application attributes, pass them through evaluationContext.attributes. The server evaluates nested object paths like user.plan.code and indexed array paths like cart.items[0].sku.

Evaluation Model

Feature flag authoring happens in layers:

  1. Flag default value -- The base value on the flag definition.
  2. Environment override -- An environment-specific configuration with its own enabled state, fallback value, and optional rollout.
  3. Targeting groups -- Ordered groups inside an environment override. Each group can return a different value when its matchers apply and can also have its own optional rollout.

Targeting groups can currently use four matcher types:

  • IP range targeting -- IPv4 or IPv6 CIDR ranges.
  • Country targeting -- ISO 3166-1 alpha-2 country codes.
  • Region targeting -- ISO 3166-2 subdivision codes.
  • Attribute targeting -- Caller-provided paths resolved from evaluationContext.attributes.

When the server evaluates a request:

  1. It picks the current organization and environment scope.
  2. It applies the enabled environment override, if one exists.
  3. It evaluates targeting groups for that environment.
  4. Matchers can combine request-derived data such as IP, country, and region with caller-provided attributes from evaluationContext.
  5. If a matching targeting group has a percentage rollout, the server performs deterministic bucketing before returning that group's value.
  6. If multiple IP groups match, the most specific CIDR wins.
  7. If specificity ties, the earlier group wins.
  8. If no targeting group resolves a value, the environment fallback value is considered.
  9. If the environment fallback has a percentage rollout, the server performs deterministic bucketing before returning that value.
  10. If no environment override applies, the base flag default value is returned.

For attribute targeting, the SDK does not infer values automatically. Your application is responsible for building the context object and updating it when the request scope changes.

The SDK never performs client-side rollout bucketing. Percentage rollout is evaluated server-side so the same logic applies across browser, server, and internal API callers.

client.setEvaluationContext({
  attributes: {
    user: {
      plan: { code: "enterprise" },
      locale: "en-US",
    },
  },
})

await client.refresh()

Durable snapshots

The SDK now persists the last successful signed evaluation automatically:

  • browsers use localStorage
  • non-browser runtimes use a local file store under the OS temp directory
  • persistence is isolated by a scope key derived from apiUrl, org, environment, team, client key fingerprint, and a canonicalized evaluationContext hash

If the flag service is unavailable later, the SDK restores the last successful snapshot for the exact same scope and keeps serving it until a newer one replaces it.

You can disable or override persistence:

import { createClient, createFileFeatureFlagSnapshotStore } from "@mergedapp/feature-flags"

const client = createClient({
  apiUrl: "<coming_soon>",
  clientKey: "lk_pub_your_key_here",
  organizationId: "org_123",
  environmentId: "env_prod",
  snapshotPersistence: {
    store: createFileFeatureFlagSnapshotStore(),
    keyPrefix: "my-app-flags",
  },
})

Set snapshotPersistence: false to disable durable snapshots entirely.

OpenFeature Integration

The native lube SDK remains the primary API. OpenFeature support is additive for teams that already standardize on OpenFeature.

Use separate runtime-specific subpaths because OpenFeature itself publishes different provider contracts for browser and server runtimes:

  • @mergedapp/feature-flags/openfeature/web
  • @mergedapp/feature-flags/openfeature/server

Web provider

import { OpenFeature } from "@openfeature/web-sdk"
import { createOpenFeatureWebProvider } from "@mergedapp/feature-flags/openfeature/web"

await OpenFeature.setProviderAndWait(
  createOpenFeatureWebProvider({
    apiUrl: "<coming_soon>",
    clientKey: "lk_pub_your_key_here",
    organizationId: "org_123",
    environmentId: "env_prod",
  }),
)

Server provider

import { OpenFeature } from "@openfeature/server-sdk"
import { createOpenFeatureServerProvider } from "@mergedapp/feature-flags/openfeature/server"

await OpenFeature.setProviderAndWait(
  createOpenFeatureServerProvider({
    apiUrl: "<coming_soon>",
    clientKey: "lk_sec_your_key_here",
    organizationId: "org_123",
    environmentId: "env_prod",
  }),
)

The default context mapper:

  • maps OpenFeature targetingKey to evaluationContext.attributes.subject.key
  • copies other OpenFeature fields into evaluationContext.attributes
  • converts Date values to ISO strings
  • merges request context over any provider-level base evaluationContext

The package also exports helper hooks:

  • createStaticContextHook()
  • createLoggingHook()

Tracking is intentionally not supported yet.

NestJS Integration

Use the @mergedapp/feature-flags/nestjs subpath when you want Nest-native decorators, guards, interceptors, or an injectable feature-flag service.

import { Module } from "@nestjs/common"
import { FeatureFlagsModule } from "@mergedapp/feature-flags/nestjs"

@Module({
  imports: [
    FeatureFlagsModule.forRoot({
      apiUrl: "<coming_soon>",
      clientKey: "lk_sec_your_key_here",
      organizationId: "org_123",
      environmentId: "env_prod",
    }),
  ],
})
export class AppModule {}

The Nest layer is native-only and evaluates directly through the Merged SDK. If you want OpenFeature inside NestJS, use the OpenFeature Nest integration separately and point it at @mergedapp/feature-flags/openfeature/server.

Exports include:

  • FeatureFlagsModule
  • FeatureFlagsService
  • RequireFeatureFlag() for controllers/routes
  • FeatureFlagGate() for service methods
  • FeatureFlagContextInterceptor
  • FeatureFlagContext

Example controller gate:

import { Controller, Get } from "@nestjs/common"
import { RequireFeatureFlag } from "@mergedapp/feature-flags/nestjs"

@Controller("beta")
export class BetaController {
  @Get()
  @RequireFeatureFlag({ flagKey: "enableBetaDashboard" })
  list() {
    return { ok: true }
  }
}

Example service-method gate:

import { Injectable } from "@nestjs/common"
import { FeatureFlagGate, FeatureFlagsService } from "@mergedapp/feature-flags/nestjs"

@Injectable()
export class BillingService {
  constructor(public readonly featureFlags: FeatureFlagsService) {}

  @FeatureFlagGate({ flagKey: "enableNewBillingFlow" })
  async charge() {
    return { ok: true }
  }
}

createTypedHooks<FlagValues>()

Generates fully typed React hooks. The generated file exports pre-built hooks:

export const { FeatureFlagProvider, useFeatureFlag, useFeatureFlags, useFeatureFlagClient, useFeatureFlagStatus } =
  createTypedHooks<FlagValues>()

Turbo Pipeline

Add the generate command to your turbo.json so flags are regenerated before builds:

{
  "tasks": {
    "generate:flags": {
      "inputs": ["featureflags.config.json"],
      "outputs": ["src/generated/feature-flags.ts"],
      "cache": false
    },
    "build": {
      "dependsOn": ["generate:flags"]
    }
  }
}

React Integration

Use the generated typed hooks for full type safety:

import { createClient } from "./generated/feature-flags"
import { FeatureFlagProvider, useFeatureFlag, useFeatureFlags } from "./generated/feature-flags"

const client = createClient({
  apiUrl: "<coming_soon>",
  clientKey: "lk_pub_your_key_here",
  organizationId: "org_123",
  environmentId: "env_prod",
  evaluationContext: {
    attributes: {
      user: {
        plan: { code: "pro" },
      },
    },
  },
})

function App() {
  return (
    <FeatureFlagProvider blockUntilReady={false} client={client}>
      <Dashboard />
    </FeatureFlagProvider>
  )
}

function Dashboard() {
  // Fully typed: "enableDarkMode" is autocompleted, value is boolean | undefined
  const { enabled, value } = useFeatureFlag("enableDarkMode")
  const allFlags = useFeatureFlags()

  if (enabled) {
    return <DarkDashboard />
  }

  return <LightDashboard />
}

The provider calls client.initialize() on mount and client.destroy() on unmount automatically. Flags update reactively via useSyncExternalStore. blockUntilReady is required so each integration chooses whether the app should render immediately or wait for the first flag payload.

React Subpath

The @mergedapp/feature-flags/react subpath is intentionally low-level. It exports FeatureFlagProvider and createTypedHooks() so generated bindings can be created, but applications should import useFeatureFlag, useFeatureFlags, useFeatureFlagClient, and useFeatureFlagStatus from their generated ./generated/feature-flags file.

SSR Hydration

Pass pre-evaluated flags to avoid a fetch on the server:

function App({ serverFlags }: { serverFlags: EvaluatedFlag[] }) {
  return (
    <FeatureFlagProvider blockUntilReady={false} client={client} initialFlags={serverFlags}>
      <Dashboard />
    </FeatureFlagProvider>
  )
}

When initialFlags is provided, the provider renders with those values immediately. Once the client initializes on the client side, live flags take over seamlessly.

Configuration Options

| Option | Type | Default | Description | | --------------------- | ------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | apiUrl | string | (required) | Base URL for the feature flag API | | clientKey | string | (required) | API key with lk_pub_* or lk_sec_* prefix | | organizationId | string | (required) | Organization scope sent to the signed evaluation endpoint | | environmentId | string | (required) | Environment scope sent to the signed evaluation endpoint | | teamId | string | - | Optional team scope sent to the signed evaluation endpoint | | publicKey | string | auto-fetch | PEM-format ES256 public key for JWT verification | | refreshInterval | number | 60000 | Polling interval in ms. Set to 0 to disable polling. | | snapshotPersistence | false \| { store?: FeatureFlagSnapshotStore; keyPrefix?: string } | auto | Durable last-known-good snapshot persistence. Browser defaults to localStorage; non-browser defaults to a file store in the OS temp dir. | | evaluationContext | FeatureFlagEvaluationContext | - | Optional caller-provided context used for attribute targeting | | flagIds | Record<string, string> | - | Mapping of stable code keys to flag IDs. Auto-configured when using generated createClient. | | onError | (error: Error) => void | - | Called when a refresh or verification error occurs | | onFlagsChanged | (flags: EvaluatedFlag[]) => void | - | Called when flag values change after a refresh |

evaluationContext uses this shape:

type FeatureFlagEvaluationContext = {
  attributes?: Record<string, unknown>
}

Recommended usage:

  • Put application-owned targeting inputs in attributes.
  • Use nested objects for stable domain structure, for example user.plan.code.
  • Use arrays only when position matters or when you plan to target with list membership.
  • Treat client-provided values as targeting inputs, not as authorization guarantees.

Error Handling

The SDK exports three error classes:

  • FeatureFlagError -- Base class for all SDK errors.
  • FeatureFlagNetworkError -- Thrown when the API is unreachable or returns a non-2xx status.
  • FeatureFlagVerificationError -- Thrown when JWT verification fails (expired, wrong issuer, tampered).

On refresh failure, the client applies exponential backoff starting at 5 seconds, capped at 5 minutes. The onError callback is invoked on every failure. Existing flags remain available during outages, including restored persisted snapshots when the evaluation scope matches exactly.

Browser tab visibility: Polling is automatically paused when the tab is hidden and resumed with an immediate refresh when it becomes visible again. This prevents unnecessary server requests from background tabs.

import { FeatureFlagNetworkError, FeatureFlagVerificationError } from "@mergedapp/feature-flags"
import { createClient } from "./generated/feature-flags"

const client = createClient({
  apiUrl: "<coming_soon>",
  clientKey: "lk_pub_your_key_here",
  organizationId: "org_123",
  environmentId: "env_prod",
  onError: (error) => {
    if (error instanceof FeatureFlagNetworkError) {
      console.warn("Flag service unreachable, using cached flags")
    }
    if (error instanceof FeatureFlagVerificationError) {
      console.error("Flag token verification failed", error)
    }
  },
})

Audit & Cleanup

Audit

Scan your codebase for unused or stale flag references:

npx merged-ff audit --api-url=<coming_soon> --client-key=lk_pub_xxx --dir=./src

Produces a report showing active, unused, and archived-but-still-referenced flags.

Cleanup

Automatically replace archived boolean flag checks with false:

# Preview changes without modifying files
npx merged-ff cleanup --api-url=<coming_soon> --client-key=lk_pub_xxx --dry-run

# Apply changes
npx merged-ff cleanup --api-url=<coming_soon> --client-key=lk_pub_xxx

After cleanup, run merged-ff generate to update the generated file.

API Reference

MergedFeatureFlags<TFlags>

The core client class is generic: MergedFeatureFlags<TFlags extends FlagRegistry = FlagRegistry>. When using generated createClient, the type parameter is pre-filled.

| Method | Returns | Description | | ----------------------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | initialize() | Promise<void> | Fetch public key (if needed), fetch and verify flags | | isEnabled<K extends string & keyof TFlags>(name: K) | boolean | Check if a flag is enabled (false for unknown flags) | | getValue<K extends string & keyof TFlags>(name: K) | TFlags[K] \| undefined | Get a flag's value with inferred return type | | getFlag(idOrName: string) | EvaluatedFlag \| undef | Get the full evaluated flag entry | | getAllFlags() | EvaluatedFlag[] | Get all evaluated flags | | refresh() | Promise<void> | Manually trigger a flag refresh from the server | | getStatus() | FeatureFlagRuntimeStatus | Read snapshot source, staleness, and last refresh metadata | | setEvaluationContext(context) | void | Replace the caller-provided attribute context for future refreshes; the current snapshot stays active until the next refresh | | onChange(listener) | () => void | Subscribe to flag changes; returns unsubscribe function | | destroy() | void | Clean up timers, listeners, and cached data |

React Hooks (Generated)

When using hooks from the generated file (via createTypedHooks<FlagValues>()), all hooks are fully typed:

| Hook | Returns | Description | | ------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | useFeatureFlag(name) | { enabled: boolean, value: TFlags[K] \| undefined } | Get a single flag's typed state | | useFeatureFlags() | EvaluatedFlag[] | Get all flags | | useFeatureFlagClient() | MergedFeatureFlags<TFlags> | Access the underlying typed client | | useFeatureFlagStatus() | { status, isLoading, isReady, error, source, isStale, lastSuccessfulRefreshAt, tokenExpiresAt } | Read provider initialization and snapshot-source state |

There is no public untyped React hook entrypoint. React applications are expected to consume the generated bindings so invalid flag code keys fail at compile time.

OpenFeature

OpenFeature integrations live on dedicated runtime entrypoints:

  • @mergedapp/feature-flags/openfeature/web
  • @mergedapp/feature-flags/openfeature/server

Use the web entrypoint with @openfeature/web-sdk and the server entrypoint with @openfeature/server-sdk. The providers support:

  • Merged evaluation through OpenFeature
  • OpenFeature domains
  • server transaction context
  • hook helpers via createLoggingHook and createStaticContextHook

OpenFeature hooks are evaluation lifecycle middleware. They let you run shared logic before evaluation, after evaluation, on error, and finally. In this SDK, the bundled helpers are meant for two common cases:

  • createStaticContextHook to attach shared context once instead of repeating it at every callsite
  • createLoggingHook to observe evaluation outcomes and failures consistently

Tracking is not implemented yet.

NestJS

NestJS integration lives at @mergedapp/feature-flags/nestjs.

It exposes:

  • FeatureFlagsModule
  • FeatureFlagsService
  • RequireFeatureFlag
  • FeatureFlagGate
  • FeatureFlagGuard
  • FeatureFlagContextInterceptor
  • FeatureFlagContext

The Nest module is native-only. It evaluates through the Merged SDK and keeps type-safe decorators, guards, interceptors, and generated bindings aligned with the generated registry.

CLI Commands

| Command | Description | | ---------- | --------------------------------------------------- | | generate | Generate typed TypeScript SDK from flag definitions | | audit | Scan codebase for unused or stale flag references | | cleanup | Replace archived boolean flag checks with false |