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-node

v0.0.1-alpha.2

Published

Server-side SDK for Lucerna Gates. It downloads the environment's compiled rule runtime with a **secret server key**, keeps it fresh with a 10-second ETag poll, and answers every flag / experiment / kill-switch check locally and synchronously — evaluation

Readme

@lucerna-dev/gates-node

Server-side SDK for Lucerna Gates. It downloads the environment's compiled rule runtime with a secret server key, keeps it fresh with a 10-second ETag poll, and answers every flag / experiment / kill-switch check locally and synchronously — evaluation is in-process (@lucerna-dev/gates-core), so reads cost no network round trip. Runs on Node 18+, Bun, Deno and edge runtimes.

Prefer per-request evaluation over a background poller? The same client also offers remote evaluation: async *Async methods that ask the server per call, with mode: "remote" when you want no poller at all.

Install

pnpm add @lucerna-dev/gates-node

Quickstart

import { createGates } from "@lucerna-dev/gates-node";

const gates = createGates({ serverKey: "ck_srv_YOUR_KEY" });

// Optional — reads are safe before it settles (they fail open).
await gates.ready();

const identity = { userId: "u_42", traits: { plan: "pro" } };

const showNewBilling = gates.flag("new_billing", identity); // boolean
const variant = gates.experiment("checkout_test", identity); // "one_page" | null
const paymentsKilled = !gates.switch("payments"); // kill switch thrown?

// Shutdown: stops the poll and flushes queued exposures.
await gates.close();

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

The server key is a secret — it downloads your targeting rules. Keep it in server-side configuration, never in code that ships to a browser. createGates takes the server key (ck_srv_…) or a scoped key (ck_key_…) granted gates:runtime, and throws at construction on the publishable ck_client_… key — that key can never download rules, and failing here beats a 403 in production. Find the server key in Settings → API keys.

API

createGates(options)GatesClient.

| Option | Default | What it does | | ------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | serverKey | required | Secret ck_srv_… / ck_key_… key; pins the environment | | baseUrl | https://api.uselucerna.app | API origin, for self-hosted or local development | | mode | "poll" | "remote" never constructs the poller or the exposure queue — nothing to close; use the *Async methods (sync reads answer safe defaults, with a one-time onError warning) | | remoteCacheTtlMs | 0 | Memoize *Async answers in-memory this long. Kill switches default to a 10s memo and are hard-capped at 10s — kills promise ≤10s propagation | | refreshIntervalMs | 10000 | Runtime poll cadence, floored at 1s. The 10s default is a product guarantee — kills propagate in ≤10s — raise it only if you accept slower kill propagation | | requestTimeoutMs | 5000 | Per-attempt request timeout | | onError | — | Tap for refresh/auth/exposure failures — reads themselves never throw | | fetch | global fetch | Transport override (tests, custom dispatchers) | | trackExposures | true | Report experiment exposures to Lucerna; false opts out (tests, or when you pipe exposures yourself) |

| Method | What it does | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- | | flag(key, identity?) | booleanfalse for unknown keys or before the runtime loads | | experiment(key, identity?) | Assigned variant name, or null when not in the experiment | | switch(key) | false means killed; unknown keys are not killed | | inspect(key, identity?) | Decision plus explain-mode trace across every namespace the key matches — for debugging targeting, never branch on it | | evaluate(identity?) | Every decision for one identity — the SSR/bootstrap projection | | flagAsync(key, identity?) | Server-evaluated flag decision — Promise<boolean>, false on unknown keys or any failure | | experimentAsync(key, identity?) | Server-evaluated assignment — Promise<string \| null>; the exposure is recorded server-side on the same request | | switchAsync(key) | Server-evaluated kill state — Promise<boolean>, memoized at most 10s | | evaluateAsync(identity?) | Every decision in one round trip — the serverless primitive; same shape as evaluate() | | runtime() | The last-known compiled runtime; undefined before the first load | | ready() | Resolves after the first successful runtime load; rejects on a bad key (401/403) | | close() | Async: stops the poll and flushes queued exposures; the last-known runtime keeps answering reads |

Identity and decision types (GatesIdentity, GatesDecisions, GatesRuntime, …) are re-exported from @lucerna-dev/gates-core — no second install. @lucerna-dev/identity is not required: any { userId, traits } object works.

Remote evaluation (no polling)

Polling is one consumption style, not the only one. Every client also carries async *Async methods that evaluate on the server per call — no runtime download, no background timer, nothing to close. Pick per call site: a long-lived server can poll and still use evaluateAsync where it wants the server's answer; a process that doesn't want a poller at all constructs with mode: "remote".

The *Async methods keep the sync guarantees: they never reject — network failure, timeout, unknown key, bad response all resolve the safe default (false / null / not killed / empty decisions) and surface through onError. Remote calls make at most 2 attempts (5s timeout each) to bound request latency. In remote mode the key needs the gates:evaluate grant rather than gates:runtime; the scaffolded ck_srv_… keys carry both.

Serverless (Lambda, Vercel, Cloudflare Workers) — construct once at module scope, then one round trip per invocation:

import { createGates } from "@lucerna-dev/gates-node";

const gates = createGates({ serverKey: process.env.LUCERNA_KEY!, mode: "remote" });

export async function handler(request: Request) {
  const identity = { userId: currentUserId(request), traits: { plan: "pro" } };

  // Every decision in one round trip — branch on the result.
  const decisions = await gates.evaluateAsync(identity);

  if (decisions.kills.payments === false) return maintenanceResponse();
  return render({ newBilling: decisions.flags.new_billing === true });
}
// No close() — there is nothing running.

SSR / long-lived servers that don't want a poller — same construction, evaluate per request; set remoteCacheTtlMs to bound round trips on a warm process (flags change infrequently — many can afford 30–60s):

const gates = createGates({
  serverKey: process.env.LUCERNA_KEY!,
  mode: "remote",
  remoteCacheTtlMs: 30_000, // kill switches stay capped at 10s
});

app.get("/checkout", async (req, res) => {
  if (!(await gates.switchAsync("payments"))) return res.status(503).send("maintenance");
  const variant = await gates.experimentAsync("checkout_test", identityOf(req));
  res.render(variant === "one_page" ? "checkout-one-page" : "checkout");
});

experimentAsync records the exposure server-side on the same request (unless trackExposures: false) — no queue, no flush. evaluateAsync never records exposures, mirroring sync evaluate().

Don't forget the await. gates.flagAsync(…) without await is a Promise — always truthy, so if (gates.flagAsync("x")) takes the branch for everyone. The Async suffix at the call site is the visual cue.

Ruby and Elixir SDK parity is planned.

Guarantees & semantics

  • Reads never throw and fail open: flag → false, experiment → null, switch → not killed. Before the first load — or if every refresh since has failed — they answer from the last-known runtime: stale beats default, default beats crash.
  • Kills propagate in ≤10s at the default poll interval. The poll is cheap: ETag / If-None-Match, so an unchanged runtime is a 304.
  • Runtime swaps are atomic. Readers see either the old runtime or the new one, never a half-applied document. A runtime with a newer schemaVersion than this SDK understands is rejected whole (reported through onError, last-known keeps answering) — an engine never half-evaluates a document it doesn't fully understand.
  • Exposure-on-read. An exposure is recorded only when your code reads a variant via experiment() — never on refreshes, evaluate() or inspect(). Exposures are deduped locally (10-minute window; the server dedupes forever), batched (≤100 per request), and delivered fire-and-forget on the poll tick — they can never block or break a read. trackExposures: false opts out.
  • Assignments are stable. Bucketing is frozen: murmur3 (x86 32-bit) over ${salt}:${unitId}, basis points (hash % 10000), stored salts — renaming a key never reshuffles users. Experiment salts are per iteration, so restarting an iteration reshuffles by design.
  • Server decisions still aren't entitlements. Gate UX and code paths with them; anything entitlement-shaped belongs in your authorization layer.

Errors & failure modes

  • Runtime requests retry network errors and 5xx up to 3 attempts with jittered exponential backoff (250ms base, 5s timeout per attempt). 4xx are terminal for the attempt — the same request will not do better. The poll loop itself is the long-cycle retry.
  • ready() rejects only on 401/403 (GatesRequestError with status), so a misconfigured key stays discoverable. The poll continues after the rejection — a key created or re-enabled later starts working without a restart.
  • onError sees every refresh, auth and exposure-delivery failure. An onError that itself throws is swallowed — it never breaks the SDK.
  • A failed exposure batch is dropped, not retried — exposures are best-effort telemetry, and the server-side dedupe makes any resend harmless.