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

featurectl

v0.4.1

Published

Remote control for your live app. Feature flags, remote config, kill switches — one line of code.

Downloads

42

Readme

featurectl

npm version license bundle size

Remote control for your live app. Feature flags, remote config, kill switches -- one line of code.

Table of Contents

Installation

npm install featurectl
yarn add featurectl
pnpm add featurectl

If you plan to use React hooks, make sure you have React 18 or later installed. It is an optional peer dependency.

Quick Start

import { initFeaturectl, flag } from "featurectl";

await initFeaturectl({ apiKey: "your-api-key" });

if (await flag("new-checkout")) {
  showNewCheckout();
}

That is all you need. initFeaturectl connects to the featurectl API, fetches your flags and configs, and keeps them in sync via real-time streaming. The flag function returns a boolean you can use anywhere in your code.

Configuration

Both initFeaturectl and createFeaturectl accept a FeaturectlOptions object with the following properties:

| Option | Type | Default | Description | |--------|------|---------|-------------| | apiKey | string | required | Your project's SDK key from the featurectl dashboard. | | apiUrl | string | "https://api.featurectl.app" | Base URL of the featurectl API. Override this for self-hosted instances or local development. | | pollInterval | number | 30000 | Interval in milliseconds between background polls. Set to 0 to disable polling entirely. | | streaming | boolean | true | Enable real-time updates via Server-Sent Events (SSE). | | defaultContext | Record<string, string> | undefined | Key-value pairs merged into every evaluation request. Useful for passing a user ID or environment. | | onRefresh | (data: EvaluateResponse) => void | undefined | Called every time flag and config data is refreshed from the server. | | onError | (error: unknown) => void | undefined | Called when a fetch to the API fails. If not provided, errors are silently swallowed and the SDK falls back to cached values. |

Full example

import { initFeaturectl, flag, config } from "featurectl";

await initFeaturectl({
  apiKey: "your-api-key",
  apiUrl: "https://api.featurectl.app",
  pollInterval: 60000,
  streaming: true,
  defaultContext: {
    userId: "user_123",
    environment: "production",
  },
  onRefresh: (data) => {
    console.log("Flags refreshed:", Object.keys(data.flags).length);
  },
  onError: (error) => {
    console.error("featurectl fetch failed:", error);
  },
});

Streaming vs Polling

By default, featurectl uses both streaming and polling to keep your flags up to date:

  • Streaming (streaming: true) opens a persistent SSE connection to the server. When you change a flag in the dashboard, the SDK receives an update event and refreshes immediately. If the connection drops, it automatically reconnects after 5 seconds.
  • Polling (pollInterval: 30000) acts as a safety net. Every 30 seconds (with a random jitter of +/-20% to avoid thundering herd), the SDK fetches the latest data from the server. This ensures your app stays in sync even if the streaming connection is interrupted.

You can use them independently or together:

| Setup | Config | Best for | |-------|--------|----------| | Both (default) | streaming: true, pollInterval: 30000 | Production apps that need fast updates with a reliable fallback. | | Streaming only | streaming: true, pollInterval: 0 | Apps that need instant updates and can tolerate brief gaps if SSE drops. | | Polling only | streaming: false, pollInterval: 30000 | Environments where SSE is not supported (some proxies, serverless). | | Manual only | streaming: false, pollInterval: 0 | Full control. Call refresh() yourself when needed. |

Error Handling

When a fetch to the API fails, the SDK does not throw. Instead, it falls back to the last cached values and calls your onError callback if one is provided:

await initFeaturectl({
  apiKey: "your-api-key",
  onError: (error) => {
    // Send to your error tracking service
    Sentry.captureException(error);
  },
});

If the very first fetch fails (during initialization), the SDK still resolves and marks itself as ready. Flags will return false and configs will return their default values until a successful fetch occurs.

Client API

The client is the core of the SDK. It manages the connection to the featurectl API, caches flag and config data, and keeps everything in sync.

Creating a Client

import { createFeaturectl } from "featurectl";

const client = createFeaturectl({
  apiKey: "your-api-key",
});

createFeaturectl(options: FeaturectlOptions): FeaturectlClient creates a new client instance. It immediately starts fetching data from the API and sets up streaming and polling based on your configuration. You can also use new FeaturectlClient(options) directly.

Async vs Sync Methods

The client provides two sets of methods for reading flags and configs:

  • Async methods (flag, flagDetails, config) automatically wait for the client to be ready before returning a value. When called with a context parameter, flag and flagDetails make a fresh server call to evaluate the flag with that context. Without context, they read from the local cache.
  • Sync methods (getFlag, getConfig, getSnapshot) read directly from the in-memory cache and return immediately. They do not wait for the client to be ready. If called before the first fetch completes, flags return false and configs return their default values.

Use async methods when you need the most accurate evaluation (especially with per-request context). Use sync methods when you need synchronous access, such as inside React render functions or hot paths where you cannot await.

Async Methods

flag(key, context?)

flag(key: string, context?: Record<string, string>): Promise<boolean>

Returns whether a feature flag is enabled.

  • Without context: waits for the client to be ready, then reads from the cache.
  • With context: waits for the client to be ready, then makes a fresh server call with the provided context merged with defaultContext. This is useful for user-specific or request-specific evaluation (e.g., percentage rollouts).
const enabled = await client.flag("new-checkout");

const enabledForUser = await client.flag("new-checkout", {
  userId: "user_123",
});

If the flag is not found or the server call fails, returns false.

flagDetails(key, context?)

flagDetails(key: string, context?: Record<string, string>): Promise<FlagEvaluation>

Returns the full evaluation result for a feature flag, including the reason for the evaluation.

  • Without context: reads from the cache.
  • With context: makes a fresh server call.
const details = await client.flagDetails("new-checkout");
// { key: "new-checkout", enabled: true, reason: "rule_match" }

const userDetails = await client.flagDetails("new-checkout", {
  userId: "user_123",
});

If the flag is not found, returns { key, enabled: false, reason: "not_found" }. If the server call fails, returns { key, enabled: false, reason: "error" }.

config<T>(key, defaultValue)

config<T>(key: string, defaultValue: T): Promise<T>

Returns the value of a remote config entry, coerced to the appropriate type. If the config is not found or parsing fails, returns defaultValue.

The SDK automatically coerces values based on the valueType stored in the server:

  • "number" values are converted with Number()
  • "boolean" values are converted from true or "true"
  • "json" values are parsed with JSON.parse()
  • All other types are returned as-is
const maxItems = await client.config<number>("max-items", 10);
const theme = await client.config<string>("theme", "light");
const features = await client.config<string[]>("enabled-features", []);

refresh()

refresh(): Promise<void>

Forces an immediate fetch of all flags and configs from the server. The cache is replaced with the new data, all subscribers are notified, and the onRefresh callback is called if provided.

You do not normally need to call this. Streaming and polling handle updates automatically. Use it when you need a guaranteed fresh state, for example after a user action that you know will change flag values.

await client.refresh();

waitUntilReady()

waitUntilReady(): Promise<void>

Returns a promise that resolves once the client has completed its first fetch. If the client is already ready, it resolves immediately.

const client = createFeaturectl({ apiKey: "your-api-key" });
await client.waitUntilReady();
// Cache is now populated, sync methods are safe to use

Sync Methods

getFlag(key)

getFlag(key: string): boolean

Reads a flag value directly from the cache. Returns false if the flag is not found or the client has not completed its first fetch.

const enabled = client.getFlag("new-checkout");

getConfig<T>(key, defaultValue)

getConfig<T>(key: string, defaultValue: T): T

Reads a config value directly from the cache with the same type coercion as the async config() method. Returns defaultValue if the config is not found or parsing fails.

const maxItems = client.getConfig<number>("max-items", 10);

getSnapshot()

getSnapshot(): {
  flags: Record<string, FlagEvaluation>;
  configs: Record<string, ConfigEvaluation>;
  version: number;
}

Returns the entire cache as a plain object. The version field increments each time the cache is updated. This is used internally by the React hooks to detect changes via useSyncExternalStore.

const snapshot = client.getSnapshot();
console.log(snapshot.flags["new-checkout"]?.enabled);
console.log(snapshot.configs["max-items"]?.value);
console.log("Cache version:", snapshot.version);

isReady()

isReady(): boolean

Returns true if the client has completed its first fetch, false otherwise.

if (client.isReady()) {
  const enabled = client.getFlag("new-checkout");
}

subscribe(listener)

subscribe(listener: () => void): () => void

Registers a callback that is called whenever the cache is updated (after a successful fetch, poll, or streaming event). Returns an unsubscribe function.

const unsubscribe = client.subscribe(() => {
  console.log("Cache updated, version:", client.getSnapshot().version);
});

// Later, stop listening
unsubscribe();

destroy()

destroy(): void

Stops all background activity: cancels polling timers and closes the streaming connection. Call this when the client is no longer needed to avoid resource leaks.

client.destroy();

Singleton API

The singleton API provides a convenient way to use featurectl without passing a client instance around. It creates a single global instance that you initialize once at startup and then access from anywhere in your codebase.

Initialization

import { initFeaturectl } from "featurectl";

await initFeaturectl({ apiKey: "your-api-key" });

initFeaturectl(options: FeaturectlOptions): Promise<FeaturectlClient> creates a global FeaturectlClient and waits until its first fetch completes. It accepts the same options as createFeaturectl (see Configuration). The returned promise resolves with the underlying client instance, which you can ignore in most cases.

You must call initFeaturectl before using any of the convenience functions below. If you call flag, config, flagDetails, refresh, or destroy before initialization, they throw an error:

Error: featurectl not initialized. Call initFeaturectl() first.

Convenience Functions

All convenience functions delegate to the global client instance created by initFeaturectl. They have the same signatures and behavior as their client counterparts.

flag(key, context?)

import { flag } from "featurectl";

const enabled = await flag("new-checkout");
const enabledForUser = await flag("new-checkout", { userId: "user_123" });

Returns Promise<boolean>. See client.flag() for full details on context behavior.

config<T>(key, defaultValue)

import { config } from "featurectl";

const maxItems = await config<number>("max-items", 10);
const theme = await config<string>("theme", "light");

Returns Promise<T>. See client.config() for type coercion details.

flagDetails(key, context?)

import { flagDetails } from "featurectl";

const details = await flagDetails("new-checkout");
// { key: "new-checkout", enabled: true, reason: "rule_match" }

Returns Promise<FlagEvaluation>. See client.flagDetails() for the full return type.

refresh()

import { refresh } from "featurectl";

await refresh();

Forces an immediate fetch of all flags and configs. Returns Promise<void>.

destroy()

import { destroy } from "featurectl";

destroy();

Stops all background activity (polling, streaming) and clears the global instance. After calling destroy, you must call initFeaturectl again before using any other singleton function.

Complete Example

// app-init.ts -- call once at startup
import { initFeaturectl } from "featurectl";

await initFeaturectl({
  apiKey: "your-api-key",
  defaultContext: { environment: "production" },
  onError: (error) => console.error("featurectl error:", error),
});

// anywhere-else.ts -- use from any file
import { flag, config } from "featurectl";

export async function getCheckoutPage() {
  const useNewCheckout = await flag("new-checkout");
  const maxCartItems = await config<number>("max-cart-items", 50);

  return {
    variant: useNewCheckout ? "v2" : "v1",
    maxCartItems,
  };
}

Singleton vs Client

| | Singleton | Client | |---|---|---| | Setup | initFeaturectl(options) once | createFeaturectl(options) per instance | | Access | Import flag, config, etc. from "featurectl" | Pass the client instance around | | Number of instances | One global instance | As many as you need | | Best for | Most apps with a single API key | Multi-tenant apps, testing, or when you need multiple clients with different configs | | React | Not used with React hooks (use the client directly) | Pass to FeaturectlProvider |

For most applications, the singleton API is the simplest choice. Use the client directly when you need more than one instance, want explicit dependency injection, or are building a React app with the provider pattern.

React

The React integration provides hooks that subscribe to flag and config changes and automatically re-render your components when values update. It is available as a separate import path.

import { FeaturectlProvider, useFlag, useConfig } from "featurectl/react";

React 18 or later is required. It is listed as an optional peer dependency, so you need to have it installed in your project already.

Setup

Create a client with createFeaturectl and pass it to FeaturectlProvider at the root of your component tree:

import { createFeaturectl } from "featurectl";
import { FeaturectlProvider } from "featurectl/react";

const client = createFeaturectl({ apiKey: "your-api-key" });

function App() {
  return (
    <FeaturectlProvider client={client}>
      <YourApp />
    </FeaturectlProvider>
  );
}

FeaturectlProvider accepts a single client prop (a FeaturectlClient instance) and makes it available to all hooks below via React context.

Hooks

useFlag(key)

useFlag(key: string): boolean

Returns whether a feature flag is enabled. The component re-renders whenever the flag value changes.

Returns false if the flag is not found or the client has not yet completed its first fetch.

import { useFlag } from "featurectl/react";

function Checkout() {
  const useNewCheckout = useFlag("new-checkout");

  return useNewCheckout ? <NewCheckout /> : <OldCheckout />;
}

useConfig(key, defaultValue)

useConfig<T>(key: string, defaultValue: T): T

Returns the value of a remote config entry. The component re-renders whenever the config value changes.

Returns defaultValue if the config is not found or the client has not yet completed its first fetch.

import { useConfig } from "featurectl/react";

function ProductList() {
  const maxItems = useConfig<number>("max-items", 10);

  return <Grid items={products.slice(0, maxItems)} />;
}

useFlagDetails(key)

useFlagDetails(key: string): FlagEvaluation

Returns the full evaluation result for a flag, including the reason. The return type is FlagEvaluation:

{
  key: string;
  enabled: boolean;
  reason: string;
}

This hook uses a ref-based cache internally to prevent unnecessary re-renders. The component only re-renders when the key, enabled, or reason fields actually change, not on every cache update.

Returns { key, enabled: false, reason: "not_found" } if the flag does not exist.

import { useFlagDetails } from "featurectl/react";

function FeatureDebug({ flagKey }: { flagKey: string }) {
  const details = useFlagDetails(flagKey);

  return (
    <div>
      <p>Flag: {details.key}</p>
      <p>Enabled: {details.enabled ? "Yes" : "No"}</p>
      <p>Reason: {details.reason}</p>
    </div>
  );
}

useFeaturectl()

useFeaturectl(): FeaturectlClient

Returns the underlying FeaturectlClient instance from context. Use this when you need direct access to the client, for example to call refresh() or destroy().

Throws an error if called outside of a FeaturectlProvider.

import { useFeaturectl } from "featurectl/react";

function RefreshButton() {
  const client = useFeaturectl();

  return <button onClick={() => client.refresh()}>Refresh flags</button>;
}

Complete Example

// client.ts
import { createFeaturectl } from "featurectl";

export const featurectlClient = createFeaturectl({
  apiKey: "your-api-key",
});

// App.tsx
import { FeaturectlProvider } from "featurectl/react";
import { featurectlClient } from "./client";

function App() {
  return (
    <FeaturectlProvider client={featurectlClient}>
      <Dashboard />
    </FeaturectlProvider>
  );
}

// Dashboard.tsx
import { useFlag, useConfig } from "featurectl/react";

function Dashboard() {
  const showBetaBanner = useFlag("beta-banner");
  const maxWidgets = useConfig<number>("max-widgets", 5);

  return (
    <div>
      {showBetaBanner && <BetaBanner />}
      <WidgetGrid max={maxWidgets} />
    </div>
  );
}

Next.js and SSR

All hooks are built on React's useSyncExternalStore, which supports server-side rendering out of the box. During server rendering, the hooks return safe default values since the client cannot connect to the featurectl API on the server:

| Hook | Server snapshot | |------|----------------| | useFlag(key) | false | | useConfig(key, defaultValue) | defaultValue | | useFlagDetails(key) | { key, enabled: false, reason: "not_found" } |

This means your server-rendered HTML will always use the fallback values. Once the app hydrates on the client and the featurectl client completes its first fetch, the hooks will re-render with the real values.

Next.js App Router

In the Next.js App Router, all components are server components by default. Since FeaturectlProvider uses React context and hooks, it must be placed in a client component. Create a wrapper with the "use client" directive:

// providers.tsx
"use client";

import { FeaturectlProvider } from "featurectl/react";
import { createFeaturectl } from "featurectl";

const client = createFeaturectl({ apiKey: "your-api-key" });

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <FeaturectlProvider client={client}>
      {children}
    </FeaturectlProvider>
  );
}

// layout.tsx
import { Providers } from "./providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Any component that uses featurectl hooks (useFlag, useConfig, etc.) must also be a client component or be rendered inside one.

Types

The SDK exports the following TypeScript types from featurectl:

import type {
  FeaturectlOptions,
  FlagEvaluation,
  ConfigEvaluation,
  EvaluateResponse,
} from "featurectl";

FeaturectlOptions

Configuration object passed to initFeaturectl or createFeaturectl. See Configuration for a full description of each field.

| Field | Type | |-------|------| | apiKey | string | | apiUrl | string \| undefined | | pollInterval | number \| undefined | | streaming | boolean \| undefined | | defaultContext | Record<string, string> \| undefined | | onRefresh | (data: EvaluateResponse) => void \| undefined | | onError | (error: unknown) => void \| undefined |

FlagEvaluation

Returned by flagDetails, useFlagDetails, and present in the flags map of EvaluateResponse.

| Field | Type | Description | |-------|------|-------------| | key | string | The flag key. | | enabled | boolean | Whether the flag is on or off. | | reason | string | Why the flag resolved to this value (e.g. "rule_match", "not_found", "error"). |

ConfigEvaluation

Present in the configs map of EvaluateResponse.

| Field | Type | Description | |-------|------|-------------| | key | string | The config key. | | value | unknown | The raw value stored on the server. | | valueType | string | The type hint used for coercion ("number", "boolean", "json", or "string"). |

EvaluateResponse

The full response shape returned from the featurectl API. Passed to the onRefresh callback and available via getSnapshot().

| Field | Type | Description | |-------|------|-------------| | flags | Record<string, FlagEvaluation> | All flag evaluations keyed by flag name. | | configs | Record<string, ConfigEvaluation> | All config evaluations keyed by config name. |

Patterns

Real-world examples showing common use cases.

Feature Toggle

Show or hide a feature based on a flag. Toggle it from the dashboard with zero downtime.

import { flag } from "featurectl";

if (await flag("new-checkout")) {
  showNewCheckout();
}

In React:

import { useFlag } from "featurectl/react";

function Checkout() {
  const newCheckout = useFlag("new-checkout");

  return newCheckout ? <NewCheckout /> : <OldCheckout />;
}

Remote Configuration

Change values in production without redeploying. Useful for limits, URLs, copy, and other settings you want to adjust on the fly.

import { config } from "featurectl";

const maxRetries = await config("max-retries", 1);
const apiUrl = await config("api-url", "https://fallback.example.com");

Kill Switch

Disable a broken feature from anywhere, even from your phone at 3am. Flip the flag in the dashboard and the change takes effect within seconds.

import { flag } from "featurectl";

if (!(await flag("payments-enabled"))) {
  return showMaintenancePage();
}

processPayment();

Percentage Rollout

Roll out a feature to a percentage of users by passing a context with a user identifier. The server uses this to determine consistent assignment.

import { flag } from "featurectl";

if (await flag("new-checkout", { userId })) {
  return renderNewCheckout();
}

return renderOldCheckout();

You can also set the user context globally so every evaluation includes it automatically:

import { initFeaturectl, flag } from "featurectl";

await initFeaturectl({
  apiKey: "your-api-key",
  defaultContext: { userId: currentUser.id },
});

// No need to pass context on every call
if (await flag("new-checkout")) {
  return renderNewCheckout();
}

Cleanup

The SDK runs background tasks (polling timers, SSE connections) to keep your flags in sync. When you no longer need the client, call destroy() to stop all background activity and free resources.

Single-page applications

If your app creates a client on mount, make sure to destroy it on unmount to prevent memory leaks:

// Vanilla JS
const client = createFeaturectl({ apiKey: "your-api-key" });

// When the app is shutting down
client.destroy();

React

When using the client with FeaturectlProvider, clean up in a useEffect return:

import { createFeaturectl } from "featurectl";
import { FeaturectlProvider } from "featurectl/react";
import { useEffect, useRef } from "react";

function App() {
  const clientRef = useRef(createFeaturectl({ apiKey: "your-api-key" }));

  useEffect(() => {
    return () => clientRef.current.destroy();
  }, []);

  return (
    <FeaturectlProvider client={clientRef.current}>
      <YourApp />
    </FeaturectlProvider>
  );
}

Singleton

For the singleton API, call the exported destroy function:

import { destroy } from "featurectl";

destroy();

After calling destroy, you must call initFeaturectl again before using any other singleton function.