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/identity

v0.0.1-alpha.1

Published

The Lucerna identity primitive: who is the current user and what do we know about them. Product SDKs consume the record and react to changes — `@lucerna-dev/gates-browser` refetches decisions on every change, and any Gates read accepts `identity.current()

Downloads

55

Readme

@lucerna-dev/identity

The Lucerna identity primitive: who is the current user and what do we know about them. Product SDKs consume the record and react to changes — @lucerna-dev/gates-browser refetches decisions on every change, and any Gates read accepts identity.current() directly. The coupling is structural, not a dependency: anything with the same { userId, traits } shape works, in either direction.

Every change also upserts the person to Lucerna People, authenticated by the required apiKey — identify once in your app and targeting, audiences and enrichment stay in sync without a second integration.

Install

pnpm add @lucerna-dev/identity

Quickstart

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

const identity = createIdentity({ apiKey: "ck_client_YOUR_KEY" });

// Logged out: a sticky anonymous id, ready for Gates bucketing.
const visitor = identity.current(); // { userId: "anon_…", anonymous: true, traits: {} }

// Sign-in: a burst of writes coalesces into one People upsert.
identity.identify({ userId: "u_42", email: "[email protected]", traits: { plan: "pro" } });
identity.trait("theme", "dark");

const user = identity.current(); // { userId: "u_42", traits: { plan: "pro", theme: "dark" }, … }

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

apiKey is any Lucerna key granted people:identify: in browsers the publishable ck_client_… key (write-only there); on servers the server key or a scoped ck_key_… key.

API

createIdentity(options)Identity.

| Option | Default | What it does | | ------------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------- | | apiKey | required | Authenticates the People upsert every change sends | | storage / storageKey | — / "lucerna:identity" | Opt-in on-device persistence (IdentityStorage) — wire only after the user consented | | baseUrl | https://api.uselucerna.app | API origin, for self-hosted or local development | | syncAnonymous | false | Also upsert anonymous visitors (off by default — every visitor would mint a ghost profile) | | onError | — | Tap for sync failures — identity itself never throws | | fetch | global fetch | Transport override (tests, custom dispatchers) |

| Method | What it does | | -------------------- | ----------------------------------------------------------------------------------------------- | | identify(input) | Declare who the user is. Same userId merges traits; a different one replaces the record | | trait(key, value) | Set one trait; null deletes it | | traits(values) | Merge several traits; null values delete | | reset() | Logout: fresh anonymous id, traits cleared, storage overwritten | | current() | A defensive copy of the IdentityUser — safe to hand to any SDK or mutate | | onChange(listener) | Subscribe to changes; returns unsubscribe |

Guarantees & semantics

  • userId is pseudonymous — your app's id (u_42), never an email. It is the id every Gates read buckets by, so it must be stable across sessions and devices.
  • PII rides separately from traits. email and name are dedicated fields the sync layer routes to People's encrypted columns; traits are plaintext targeting data everywhere they travel and must never carry raw PII. Trait values are stringified on write.
  • Users never bleed into each other. Identifying a different userId (or coming from anonymous) starts a clean record — the previous user's traits, email and name are dropped, not merged. reset() mints a fresh anonymous id.
  • Anonymous users get sticky bucketing. The anon_<uuid> id gives logged-out users stable Gates assignments for the session (and across sessions with storage). Anonymous visitors are not synced to People unless syncAnonymous: true.
  • Persistence is opt-in and consent-gated. Without a storage adapter, nothing is ever written to the device. Corrupt entries, privacy mode and quota errors fall back to a fresh in-memory identity — storage never throws into app code.
  • Upserts are coalesced and deduped. A burst of writes (identify() + several trait() calls) delivers one upsert; a payload identical to the last successful one is not resent.
  • Nothing here throws. Sync failures report through onError; a listener or error tap that itself throws is swallowed — it never breaks identity or its peers.

Errors & failure modes

The People upsert is fire-and-forget: a failed send reports through onError and leaves the dedupe slot unset, so the next change (or an identical retry) goes out again. No response is ever awaited on the write path — identify/trait/reset are synchronous and cannot fail.