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

@voxell/featherweight

v0.1.0

Published

Cross-device state for static sites — local-first, synced through a dumb edge key-value store. No framework, ~1 KB.

Readme

featherweight

Cross-device state for static sites. Your data lives in the browser (instant, offline); a low-latency edge KV store syncs it between devices. No backend, no build step, ~1 KB, zero dependencies. Works with any framework — or none; it's just a function.

<script type="module">
  import { featherweight } from './featherweight.js';   // copy the file, or use the CDN below

  const store = featherweight('my-doc');             // id = the storage key
  store.load(state => render(state));                // cache instantly, then remote if newer
  saveBtn.onclick = () => store.save({ count: 7 });  // local now, edge debounced
</script>

That's the whole idea: local-first reads, last-write-wins sync, through an edge store that has no idea what your data means.


Why

A static site has no server to remember anything. The usual answers are both too big for a lot of cases:

  • Rewrite it as an SPA — ship a framework runtime to every visitor to persist a counter. A cargo plane for a sandwich.
  • Stand up a backend — a server, a schema, a deploy, to store a blob.

featherweight is the small middle: keep the state on the client, and use the edge as a byte pipe — a key-value namespace that stores and returns one JSON blob per id. On Cloudflare that's a Pages Function + KV binding you can stand up in two minutes, and it deploys with your static site.

Install

Two pieces: the client (browser) and one edge endpoint.

1. Client — pick one

// a) Copy src/featherweight.js into your assets and import it by path:
import { featherweight } from './featherweight.js';

// b) Or from the CDN, nothing to copy:
import { featherweight } from 'https://cdn.jsdelivr.net/gh/VoxellInc/featherweight_sdk/src/featherweight.js';

// c) With a bundler (Vite/webpack/React/etc.), bare imports work once it's on
//    npm; until then point at the file or the CDN URL above.

2a. Edge (Cloudflare Pages / Workers)

Copy edge/cloudflare-pages.js to functions/api/featherweight/[[id]].js (note the double brackets for wildcard routing to handle both standard IDs and the WebSocket streaming route), then bind a KV namespace named FEATHERWEIGHT:

npx wrangler kv namespace create FEATHERWEIGHT
# Pages → Settings → Bindings → add a KV binding named FEATHERWEIGHT
# (Production + Preview), then redeploy.

An optional, high-performance in-memory real-time push sync (via a Durable Object) is supported. To enable, add a Durable Object binding named SYNC_BROKER pointing to the SyncBroker class.

A standalone Worker variant (with CORS, for a separate origin) is in edge/cloudflare-worker.js.

2b. Edge (Google Cloud Run / GCP Marketplace)

For high-availability enterprises and teams requiring unified billing, Featherweight Sync can be deployed as a containerized serverless broker on Google Cloud Run and backed by Google Cloud Firestore.

You can subscribe directly via the Google Cloud Marketplace or run the broker container yourself:

  1. GCP Marketplace Subscription:

    • Subscribe to Featherweight Sync in the GCP Console.
    • Click Register with Provider to activate your entitlement. This redirects you to the activation portal where your unique SDK apiKey (e.g. fw_live_2a98c56e...) is provisioned.
  2. Client Config with GCP Broker: Set the synchronization endpoint to your Google Cloud External Load Balancer IP and include your provisioned API key:

    import { featherweight } from 'https://cdn.jsdelivr.net/gh/VoxellInc/featherweight_sdk/src/featherweight.js';
    
    const docId = 'enterprise-settings';
    const syncStore = featherweight(docId, {
      endpoint: 'http://34.117.115.19/api/featherweight', // Your high-availability GCLB IP
      apiKey: 'YOUR_PROVISIONED_FW_LIVE_KEY',            // Your unique GCP entitlement key
      realtime: true                                      // Enable real-time WebSocket push sync
    });
  3. Enterprise Go Sync Broker: A high-performance, containerized Go Sync Broker is available for enterprise licensing and direct deployment. It supports autoscaling to zero when idle, advanced rate limiters, and direct integration with your VPC and Firestore clusters. Subscribe directly via Google Cloud Marketplace or contact Voxell, Inc. for deployment options.

3. Verify

curl https://yoursite.com/api/featherweight/ping
  • Before binding KV: {"data":null,"updatedAt":0,"nobind":true} — the nobind flag means storage isn't attached yet; the client runs local-only and nothing breaks, it just doesn't sync.
  • After binding + redeploy: the nobind flag is gone, and a PUT persists.

Local dev

npx wrangler pages dev <build-dir> runs the Function plus a local KV, so you can test real sync before deploying. A plain static file server won't have the endpoint — the client simply runs local-only against it, which is also fine.

Integrating into an existing site (field notes)

These are the things that actually bite when you drop featherweight into a real, already-built page (learned by doing exactly that — two hand-written character sheets with drag-to-spend hit points, tap-to-burn spell slots, toggled conditions, and an editable notes field):

  • Reuse the KV you already have. A live site usually already has a KV namespace. You don't need a second one — change the single KV(env) line at the top of cloudflare-pages.js to point at it (e.g. env.NOTES). The route and key prefix (fw:) keep featherweight's data from colliding with anything else in that namespace.
  • It's an ES module. From an existing classic <script> (a plain IIFE, not type="module") you can't use a static import. Either switch that block to <script type="module">, or load it dynamically: import('/featherweight.js').then(({ featherweight }) => …).
  • Mind the load gap. Dynamic import is async, so there's a brief window before the library is ready. If the user can interact in that window, don't let the initial load() clobber their input — track an "already touched" flag and, if it's set, save() their state instead of applying the loaded one. (Add <link rel="modulepreload" href="/featherweight.js"> to shrink the gap.)
  • apply() must be idempotent. load() calls it with the cached value and then again with the remote value if newer. Set state absolutely (el.classList.toggle(cls, !!on), el.value = data.x) — never append, flip, or increment from inside apply.
  • Guard fields being edited. If apply() fires (e.g. a remote sync lands) while the user is typing in an input/contenteditable, writing to it wipes their text and jumps the caret. Skip those: if (el !== document.activeElement) el.textContent = data.notes.
  • Migrating off bespoke storage? featherweight stores under fw-<id>, which won't match your old localStorage keys, so existing local state reads as empty on first load. If you need it preserved, do a one-time copy from the old key into a save() before the first load().

API

| call | does | |---|---| | featherweight(id, opts?) | create a store. opts: endpoint (default /api/featherweight), debounce (ms, default 800), onStatus(s), storage (default localStorage), realtime (boolean, default false), websocketUrl (custom WS URL). | | store.load(apply) | local-first load. apply(data, {source}) fires with the cached value, then the remote value if newer. If realtime: true, it automatically establishes a WebSocket connection for push sync. | | store.save(data) | write the cache now (survives reload instantly) + debounced PUT to the edge. Safe to call every keystroke. | | store.flush() | send a pending save immediately (use on pagehide). | | store.peek() | the current cached value, synchronously. |

data is any JSON-serializable value. The wire/record shape is { data, updatedAt }. The client's default endpoint (/api/featherweight) lines up with the edge route (/api/featherweight/<id>) out of the box.

When to use it

Use featherweight when all of these are roughly true:

  • Single writer. One person editing their own thing across their own devices (a tool, a dashboard, a tracker, a game sheet, notes).
  • Small, read-mostly state. Kilobytes, not a database.
  • Offline and instant matter. You want it to work on a plane and never spin a loader for local reads.
  • No SSR requirement. First paint doesn't need to be server-rendered for SEO.

It shines for personal apps on a CDN where a backend would be overkill — and it drops cleanly into a framework app too, since it's just a function.

When not to use it (honest)

  • Multiple concurrent writers. It's last-write-wins; two devices editing at once will clobber. If you need real merge, reach for CRDTs (Automerge, Yjs) or a sync engine (ElectricSQL, Replicache/Zero) — a different, heavier, fine tool.
  • Anything sensitive or trust-bound. The endpoint has no auth by default — possession of the id is access. Fine for a personal doc; add a capability token (in the id/URL) or real auth before storing anything you'd mind a stranger reading or overwriting.
  • Large or heavily-queried data. It stores/returns one blob; it's not a database. KV is eventually consistent (cross-region propagation up to ~60s) and has write-rate limits.
  • You already have a backend. Then just use it.

How it works

  1. Read: load() paints from localStorage synchronously, then GETs the blob from the edge and re-applies only if updatedAt is newer. The network is never in the way of showing your data.
  2. Write: save() updates the cache immediately and debounces a PUT. Offline? It stays cached and reports local-only; it syncs next time.
  3. Reconcile: last-write-wins by updatedAt. Simple on purpose.
  4. Edge: a tiny function that puts/gets a JSON string in KV by id. It's deliberately simple — it knows nothing about your data, which is what makes it cheap, cacheable, and replaceable.

The bigger picture

featherweight is the persistence piece of a broader idea — static delivery + local-first client + a low-latency edge store — written up here: The Featherweight Pattern (where it fits among SPAs and HTML-over-the-wire, plus the JSON-vs-binary numbers on whether the payload should ever be binary).

License

MIT © Voxell, Inc.