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

@lucerna-dev/gates-browser

v0.0.1-alpha.1

Published

Browser SDK for Lucerna Gates. It bootstraps identity-resolved decisions from the server with the environment's **publishable client key**, caches them in memory, and answers every flag / experiment / kill-switch check synchronously. Targeting rules never

Readme

@lucerna-dev/gates-browser

Browser SDK for Lucerna Gates. It bootstraps identity-resolved decisions from the server with the environment's publishable client key, caches them in memory, and answers every flag / experiment / kill-switch check synchronously. Targeting rules never reach the browser — only decisions do — evaluated server-side by the same engine (@lucerna-dev/gates-core) that powers @lucerna-dev/gates-node. @lucerna-dev/identity is the natural identity source, but any object with the same shape works — there is no dependency between the packages.

Install

pnpm add @lucerna-dev/gates-browser

React is an optional peer (>=18), needed only for the @lucerna-dev/gates-browser/react subpath. The root entry never imports it.

Quickstart

import { createGates } from "@lucerna-dev/gates-browser";
import { createIdentity } from "@lucerna-dev/identity";

const identity = createIdentity({ apiKey: "ck_client_YOUR_KEY" });
const gates = createGates({ clientKey: "ck_client_YOUR_KEY", identity });

// Synchronous, never throws — before the first load reads fail open.
const before = gates.flag("new_billing"); // false until decisions load

// Await the first bootstrap when the answer matters right now.
await gates.ready();

const showNewBilling = gates.flag("new_billing"); // this user's decision
const variant = gates.experiment("checkout_test"); // "one_page" | null
const paymentsKilled = !gates.switch("payments"); // kill switch thrown?

// Sign-in: decisions clear and refetch for the new user automatically.
identity.identify({ userId: "u_42", traits: { plan: "pro" } });

This block runs verbatim in test/readme.test.ts — if it drifts from the package, the test suite fails.

Never put a secret key in browser code — anyone can read it there. createGates takes the publishable ck_client_… key only and throws at construction on a secret (ck_srv_… / ck_key_…). Find the client key in Settings → API keys.

Documentation

React

import {
  Experiment,
  Feature,
  GatesProvider,
  KillSwitch,
  Variant,
} from "@lucerna-dev/gates-browser/react";

function App() {
  return (
    <GatesProvider client={gates}>
      <Feature name="new_billing" fallback={<OldBilling />}>
        <NewBilling />
      </Feature>

      <KillSwitch name="payments" fallback={<PaymentsDown />}>
        <Payments />
      </KillSwitch>

      <Experiment name="checkout_test" fallback={<Steps />}>
        <Variant name="control">
          <Steps />
        </Variant>
        <Variant name="one_page">
          <OnePage />
        </Variant>
      </Experiment>
    </GatesProvider>
  );
}

| Export | What it does | | ---------------------------- | ------------------------------------------------------------------ | | GatesProvider | Puts a client in context: <GatesProvider client={gates}> | | <Feature name fallback> | Renders children while the flag is on | | <KillSwitch name fallback> | Renders children while the path is alive; fallback when killed | | <Experiment name fallback> | Renders the assigned <Variant> child; fallback when not assigned | | <Variant name> | One variant's UI — direct child of <Experiment> | | useFlag(key) | Flag decision; false until decisions load | | useExperiment(key) | Assigned variant name; null when not in the experiment | | useKillSwitch(key) | false means the guarded path is killed | | useGates() | The client itself, for imperative calls (refresh, close) |

Everything subscribes via useSyncExternalStore — components re-render when decisions change (first load, identity switch, refresh()). Full semantics (unmatched-variant fallback, SSR, Preact) in the React guide.

Devtools (dev widget)

An in-page widget for development: a clip on the screen edge (mid-right by default; position takes ReactQueryDevtools-style corners) expanding into a searchable, full-height drawer. Every gate row opens a detail view explaining how it evaluated — server value, reason code and a plain-language explanation, plus the exact identity and environment it was computed for — next to local override toggles (flags, kill switches, forced experiment variants) and identity impersonation, all persisted under the devtools' own localStorage key. Overrides are a decorator around the real client; nothing changes server-side and, like the decisions they shadow, they are UX hints, never authorization. The handle also registers itself at window.__LUCERNA__.gates, so the console can drive it without an import.

import { createGates } from "@lucerna-dev/gates-browser";
import { createGatesDevtools } from "@lucerna-dev/gates-browser/devtools";

const devtools = createGatesDevtools();
const gates = devtools.wrap(
  createGates({
    clientKey: "ck_client_YOUR_KEY",
    // Impersonation-aware: answers the impersonated identity when one
    // is active, your real identity source otherwise.
    identity: devtools.identity(identity),
  }),
);

Mount the widget from a development-only code path — a build-time branch your bundler folds away, never runtime env sniffing:

import { GatesDevtoolsWidget } from "@lucerna-dev/gates-browser/devtools/react";

{
  import.meta.env.DEV ? <GatesDevtoolsWidget devtools={devtools} /> : null;
}

Both subpaths ship zero bytes to production when the importing branch is dead code ("sideEffects": false — the modules tree-shake cleanly). Wiring nuances (reason codes vs. full traces, forced-variant metadata, persistence) in the references.

API

createGates(options)GatesBrowserClient.

| Option | Default | What it does | | ------------------------ | ---------------------------- | -------------------------------------------------------------------------------------------- | | clientKey | required | Publishable ck_client_… key; pins the environment | | identity | — | Current-user source (IdentitySource); decisions refetch when it changes | | baseUrl | https://api.uselucerna.app | API origin, for self-hosted or local development | | storage / storageKey | — / "lucerna:gates" | Optional decisions cache for instant next load (DecisionsStorage) | | onError | — | Tap for bootstrap/refresh failures — reads themselves never throw | | requestTimeoutMs | 5000 | Per-attempt request timeout | | fetch | global fetch | Transport override (tests, custom dispatchers) |

| Method | What it does | | ----------------- | ------------------------------------------------------------------- | | flag(key) | booleanfalse when unknown or not loaded | | experiment(key) | Variant name, or null when not in the experiment | | switch(key) | false means killed; unknown keys are not killed | | decisions() | The full snapshot; undefined before the first load | | ready() | Resolves after the first successful bootstrap; rejects on a bad key | | refresh() | Refetch decisions for the current identity now; never rejects | | onChange(fn) | Fires after every decisions change; returns unsubscribe | | close() | Unsubscribes from the identity; the last decisions keep answering |

Decision types (GatesDecisions, FlagDecision, VariantAssignment, …) are re-exported from @lucerna-dev/gates-core — no second install.

Guarantees & semantics

  • Client decisions are UX hints, never authorization. Anything entitlement-shaped must be re-checked server-side (@lucerna-dev/gates-node or your API).
  • Reads never throw and fail open: flag → false, experiment → null, switch → not killed. After load, the last-known decisions keep answering — including after close() and through failed refreshes.
  • Only userId and traits are ever sent. The identity's PII fields (email, name) are deliberately stripped from the bootstrap request.
  • User switches can't leak decisions. A new userId clears decisions immediately (no flash of the previous user's variants), and a stale in-flight response can never overwrite a newer identity's decisions — the last issued request always wins.
  • Identity changes coalesce. An identify() followed by a burst of trait() calls triggers one refetch, not four.
  • No background poll in v1. A running page picks up config changes on the next identity change, refresh(), or page load. Kill switches that must cut off a page mid-session belong behind a server check.
  • The storage cache is per-user. A cached snapshot computed for another userId (or another anonymous id) is ignored, as are corrupt entries — the client just fetches fresh.
  • Assignments are stable. The server evaluates with frozen bucketing — murmur3 (x86 32-bit) over ${salt}:${unitId}, basis points (hash % 10000), stored salts — so renaming a key never reshuffles users.

Errors & failure modes

  • The bootstrap POST retries network errors and 5xx up to 3 attempts with jittered exponential backoff (250ms base, 5s timeout per attempt). 4xx are terminal — retrying can't fix a bad key.
  • ready() rejects only on 401/403 (GatesRequestError with status), so a misconfigured key stays discoverable. Any other failure reports through onError and ready() stays pending until a later refetch succeeds.
  • onError sees every bootstrap/refresh failure. An onError (or onChange listener) that itself throws is swallowed — it never breaks the SDK or its peers.
  • Without storage, decisions live for the page's lifetime; every load starts fail-open until the bootstrap answers.