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

launchflag

v0.5.0

Published

LaunchFlag feature flags client. One key, one function, edge-safe, fails safe.

Readme

launchflag

One env key, one import, one line. Edge-safe, no dependencies, fails safe.

npx launchflag init --key lf_dev_… --url https://launchflag.dev

Using pnpm? Run pnpm dlx launchflag init instead of npx.

One command: it installs this package, writes LAUNCHFLAG_KEY into .env.local, downloads the agent skill and configures the MCP server. Leave --key and --url off and it asks. --no-install skips the install step.

This package is both the SDK and the launchflag CLI. launchflag-mcp is a separate package you only need if you want the MCP server running locally instead of hosted.

import { flag } from "launchflag";
if (await flag("checkout_v2", user.id)) return <CheckoutV2 />;
return <CheckoutV1 />;

Flags register themselves. Write the if, ship it dark, flip it on the dashboard.

The first time this code runs, checkout_v2 appears on your dashboard, off in dev, staging and prod, fail closed. Until you turn it on, flag() returns false.

Manual setup

Rather do it by hand: npm i launchflag, then set LAUNCHFLAG_KEY (and LAUNCHFLAG_URL if you self-host) yourself. Keys are on the dashboard Install page.

JSX

An async server component. React 19+, optional peer dependency.

import { Flag } from "launchflag/react";
<Flag name="checkout_v2" user={user.id} fallback={<CheckoutV1 />}><CheckoutV2 /></Flag>;

Variants

A multivariate flag serves one arm per user, stable forever and split by the weights you set on the dashboard.

import { variant } from "launchflag";
const arm = await variant("pricing_test", user.id); // "a" | "b" | null

null means the flag is off for this user, unknown, or boolean. Pass a third argument for a different fallback: await variant("pricing_test", user.id, "a"). Same snapshot and cache as flag(), so it costs no extra request.

import { Variant } from "launchflag/react";
<Variant name="pricing_test" user={user.id} cases={{ a: <PriceA />, b: <PriceB /> }} fallback={<PriceA />} />;

LaunchFlag serves the arm and records nothing else. Measure it wherever you already measure things.

Local evaluation

By default the SDK fetches one document, /api/config, and decides every flag in your process. One request per ttlMs for the whole server, not per user, and it revalidates with If-None-Match so an unchanged config costs nothing. First N targeting is the one exception: admission lives in our database, so those flags call /api/eval and the answer is cached 60 seconds per user. Counts are buffered and posted to /api/counts every 10 seconds, or on flush(). Pass mode: "remote" to evaluate server-side instead, one snapshot per user.

Explicit config

import { createLaunchFlag } from "launchflag";
const lf = createLaunchFlag({ key: process.env.LAUNCHFLAG_KEY!, baseUrl: "https://flags.acme.com", defaults: { checkout_v2: false } });
await lf.flag("checkout_v2", user.id);
await lf.flag("new_nav", user.id, true); // third argument: used only when nothing is known
// lf.variant("pricing_test", user.id) → the arm · lf.all(user.id) → every flag
// lf.why("checkout_v2", user.id) → { enabled, reason, variant } · lf.flush() · lf.clear()

Client components

If you have a server, it already knows the answers. Evaluate there and hand the result down: no key in the browser, no request from the browser, nothing to flicker.

// app/layout.tsx, a server component
import { snapshot } from "launchflag";
import { LaunchFlagProvider } from "launchflag/client";

export default async function Layout({ children }) {
  const flags = await snapshot(["checkout_v2", "pricing_test"], user.id);
  return <LaunchFlagProvider snapshot={flags}>{children}</LaunchFlagProvider>;
}
"use client";
import { useFlag, useVariant } from "launchflag/client";

export function Checkout() {
  const on = useFlag("checkout_v2");            // second argument is the fallback, false by default
  const arm = useVariant("pricing_test", "a");
  return on ? <CheckoutV2 arm={arm} /> : <CheckoutV1 />;
}

snapshot(keys, user) returns { flags, variants }, plain JSON, evaluated from the same cached config as flag(). Name the keys and only those reach the browser; leave keys off for every flag in the project.

With no provider mounted, useFlag returns its fallback and useVariant returns null. A missing provider in one corner of an app is not worth a white screen.

This works anywhere React renders on a server: Next App Router and Pages Router, Remix, TanStack Start, Astro React islands. The two async server components in launchflag/react are unchanged and still work.

Without React

The snapshot is plain data on purpose. Svelte, Vue and everything else do the same thing in a few lines, so there is no package from us to install.

// server: evaluate, then serialise it into the page however your framework passes data down
const flags = await snapshot(["checkout_v2"], user.id);
// browser: read it back
const flags = JSON.parse(document.getElementById("lf")!.textContent!);
if (flags.flags.checkout_v2) …

That is the whole contract: { flags: Record<string, boolean>, variants: Record<string, string | null> }.

Static sites and SPAs

A static build has no server at runtime, so it evaluates in the browser against a publishable key. Not at build time: flipping a flag would need a rebuild, and a kill switch you have to rebuild is not a kill switch.

import { createBrowserClient } from "launchflag/browser";

export const lf = createBrowserClient({
  key: import.meta.env.VITE_LAUNCHFLAG_PUBLIC_KEY, // lfpub_prod_…, the only key that may be in browser code
  user: currentUserId,                             // optional, the bucketing seed
  defaults: { checkout_v2: false },                // what renders until the document arrives
});

lf.flag("checkout_v2");          // synchronous, never throws
lf.variant("pricing_test", "a");
lf.identify(user.id);            // after sign-in, re-evaluates locally with no request

It fetches GET /api/config/public?key=… once on start, evaluates every flag locally with the same engine the server runs, keeps the last good document in localStorage, and revalidates on a TTL (60s by default) and whenever the tab comes back into view. Nothing here is awaited on the render path.

With React, hand it to the same provider and the hooks re-render when the document lands:

<LaunchFlagProvider source={lf}>{children}</LaunchFlagProvider>

createBrowserClient is also a getSnapshot / subscribe pair, which is all a Svelte store or a Vue ref needs. npx launchflag init --public-key lfpub_prod_… writes the key with the prefix your bundler needs.

Three things to know before you expose a flag

1. An exposed flag is published. Its key, whether it is on, and its rules all go into a document any visitor can read. Exposure is per flag and off by default: a flag appears in the public document only once you turn its browser exposure on in the dashboard. Do not expose a flag whose name gives away something you have not announced.

2. User targeting is spoofable in a browser. The user id comes from the page, so anyone can claim to be anyone. Percentage rollout is fine, because bucketing is deterministic and the seed is not secret, and a spoofer only moves themselves. Everything else is a request, not a fact: a client-side flag is a UI hint and never an authorisation boundary. Gate the API, not the button. Allowlist flags cannot be exposed at all, because the addresses would have to ship with them, and first_n flags evaluate to off in a browser because admission lives in our database. Both stay server-side.

3. There is a first paint before the config arrives. On a first visit the browser paints before the fetch resolves, so it renders defaults (and false for anything not in defaults). Then the document lands and anything that changed re-renders: that is the flash people ship by accident. Three ways out, in order of preference. Set defaults to the state you would rather show, which is usually the old one, so the flash only hits users who are actually in the rollout. Return visits are already covered, because the cached document answers before the first paint. Or, if a flag decides the whole page, hold the render on await lf.ready and show your normal loading state, but never do that for a flag inside a list or a nav.

The fail-safe story

  • No call at all, most of the time. The rule set is cached for ttlMs and shared by every user, so a render costs zero requests. Concurrent calls share one fetch.
  • A slow API never blocks you. Requests abort at timeoutMs.
  • A down API never flips a flag. A failed fetch keeps serving the last known config; with nothing cached, flag() returns defaults[key], then its third argument, else false. flag(), variant(), snapshot() and flush() never throw.
  • all() and why() do throw, and only when they could not read the API. They report what the server holds, so an empty answer from them would be indistinguishable from a project with no flags, which is the reading that sends people off to register everything twice. Nothing on the eval path is affected.
  • A true fallback also creates the flag on. The first time we see a key, flag(key, user, true) (or defaults: { key: true }) creates it enabled in every environment. That is what you want when the code you are wrapping is already live: born off means the flag appearing is what switches your shipped feature off. It applies only to a key we have never seen; a flag already on your dashboard keeps whatever a person set.
  • LAUNCHFLAG_KEY is a secret. Call flag() from Server Components, route handlers or middleware only. Importing launchflag from browser code resolves to a stub that throws and tells you which entry you wanted. For client components use launchflag/client, and with no server at all launchflag/browser.
  • The browser client fails the same way. A dead or blocked API serves the last good document, then defaults, then the fallback you pass. Reads are synchronous and never throw, so nothing it does can break a render.