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

@usagebill/sdk

v0.4.0

Published

One-line wrapper around @supabase/supabase-js that tags every request per tenant — usage-based billing & per-tenant cost attribution for multi-tenant Supabase apps.

Readme

@usagebill/sdk

One line of code — and you can see which of your customers (tenants) drives which Supabase usage, and what to bill them for.

A thin wrapper around @supabase/supabase-js that tags every request with a tenantId and streams lightweight usage signals to usagebillusage-based billing & per-tenant cost attribution for multi-tenant SaaS on Supabase.

Supabase reports usage per project, never per tenant. This SDK captures what you need to bill each customer — requests, rows, egress bytes, latency, status — by hooking the one seam Supabase officially supports: a custom fetch. So it sees every call (REST, RPC, Storage, Auth) without changing how you use supabase-js.

  • One-line integration — same createClient, one extra option.
  • Fire-and-forget — tracking never blocks or throws on your user's path. If usagebill is unreachable, your app is unaffected.
  • Zero-PII by default — only path/table/operation/size metadata leaves your app. Never request/response bodies, headers, query values, IPs, or user IDs.

Install

npm install @usagebill/sdk
# pnpm add @usagebill/sdk · yarn add @usagebill/sdk

@supabase/supabase-js v2 is a peer dependency (you already have it).

Quick start

import { createClient } from "@usagebill/sdk";

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  usagebill: {
    apiKey: "ub_live_…",            // from your usagebill dashboard
    tenantId: () => getCurrentTenantId(),
  },
});

// use `supabase` exactly as before — every call is now tagged & metered
const { data } = await supabase.from("invoices").select("*");

Without the usagebill option, createClient is a pure passthrough to supabase-js.

Server-side: isolate the tenant per request

On a shared server client a plain tenantId string would mix concurrent requests → wrong invoices. Use the AsyncLocalStorage helper so each request keeps its own tenant:

import { createClient } from "@usagebill/sdk";
import { createTenantContext } from "@usagebill/sdk/server";

const tenants = createTenantContext();

const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY, {
  usagebill: { apiKey: "ub_live_…", tenantId: tenants.current },
});

// wrap each request — ALS propagates the tenant through the async chain
app.use((req, res, next) => tenants.run(resolveTenant(req), () => next()));

@usagebill/sdk/server is Node-only (it uses node:async_hooks).

With @supabase/ssr (Next.js App Router, the common setup)

If your app uses @supabase/ssr (createServerClient / createBrowserClient), don't swap the import — inject usagebill's tracked fetch into the client you already create:

// utils/supabase/server.ts — a new client is created per request
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { createUsagebillFetch } from "@usagebill/sdk";

export async function createClient() {
  const cookieStore = await cookies();
  const tenant = await resolveTenant(cookieStore); // this request's tenant (JWT claim / session)
  return createServerClient(SUPABASE_URL, SUPABASE_KEY, {
    cookies: { /* getAll / setAll as usual */ },
    global: {
      fetch: createUsagebillFetch({
        apiKey: process.env.USAGEBILL_API_KEY!,
        tenantId: () => tenant,
      }),
    },
  });
}
// utils/supabase/client.ts — browser
import { createBrowserClient } from "@supabase/ssr";
import { createUsagebillFetch } from "@usagebill/sdk";

export const createClient = () =>
  createBrowserClient(SUPABASE_URL, SUPABASE_KEY, {
    global: {
      fetch: createUsagebillFetch({
        apiKey: process.env.NEXT_PUBLIC_USAGEBILL_API_KEY!, // client key is public by nature
        tenantId: () => getCurrentTenantId(),
      }),
    },
  });

Why no AsyncLocalStorage here? With @supabase/ssr a fresh client is created per request, so the tenant is resolved per request in the getter closure. createUsagebillFetch shares one batching buffer per (endpoint, apiKey) under the hood, so calling it per request is cheap and correct. ALS (createTenantContext from @usagebill/sdk/server) is only needed when you reuse one shared singleton client across requests (e.g. a backend without ssr).

Edge runtimes (Vercel Edge Middleware, Cloudflare Workers): timers may not fire after the response completes — pass the runtime's waitUntil and usagebill delivers each request's events through it:

createUsagebillFetch({
  apiKey: process.env.USAGEBILL_API_KEY!,
  tenantId: () => tenant,
  waitUntil: (p) => ctx.waitUntil(p), // Vercel middleware: event.waitUntil
});

If a send fails, the events stay buffered and ride along with the next request's flush — but a batch whose send fails on the isolate's final request can still be lost. Delivery is best-effort with server-side dedup by event id (retries are never double-billed).

What gets captured

One UsageEvent per Supabase call:

| Field | Meaning | |---|---| | tenant | from your tenantId getter | | project | optional static projectId tag | | resource | rest · rpc · storage · auth · other | | table / op | PostgREST table, RPC name, or storage bucket & select / insert / update / delete / rpc / HTTP method (storage) | | status | HTTP status code | | durationMs | request latency | | bytes | response body size (egress proxy) | | rows | row count (from the content-range header) | | ts / id | timestamp + stable id (re-delivery is de-duplicated server-side) |

No request/response bodies, headers, query values, IPs, or user IDs are ever collected.

Options

These options are shared by both createClient (the usagebill: sub-object) and createUsagebillFetch:

| Option | Type | | |---|---|---| | apiKey | string | required — your usagebill ingest key (ub_live_…) | | tenantId | () => string \| null | required — a getter (never a bare string), so server clients never mix requests | | projectId | string? | optional static project tag | | endpoint | string? | ingest endpoint; defaults to the usagebill cloud | | waitUntil | ((p: Promise<unknown>) => void)? | edge runtimes only — pass the runtime's waitUntil ((p) => ctx.waitUntil(p)) so each request's events are delivered after the response (timers may not fire there) |

createUsagebillFetch(options)

Returns a tracked fetch function suitable for global.fetch injection. Accepts the same options as above. Buffers are shared per (endpoint, apiKey) — calling this once per request (as @supabase/ssr patterns require) is safe and cheap.

Changelog

0.4.0

  • waitUntil option — reliable delivery on edge runtimes (Vercel Edge Middleware, Cloudflare Workers): pass the runtime's waitUntil and each request's events are drained through it after the response. At-least-once with server-side dedup by event id; a batch whose send fails on the isolate's final request can still be lost.
  • sendBatch keepalive fallback: runtimes whose fetch rejects the keepalive option (Cloudflare Workers) get one retry without it — events now deliver there.
  • English API docs: all public JSDoc (npm .d.ts hovers) translated to English.

0.3.0

  • createUsagebillFetch(options) — new export for @supabase/ssr / global.fetch injection patterns; accepts the same options as createClient. Buffers are shared per (endpoint, apiKey) so calling this once per request is safe and cheap.

0.2.0

  • Storage events now carry the bucket name in table, parsed from the request path — bucket only, never object paths (zero-PII). Dashboard storage rows and Stripe meters can now target individual buckets (see per-bucket billing below). Calls whose bucket lives only in the request body (copy/move) keep table: null.
  • Per-bucket billing: meters filtered on table only match REST/RPC traffic by default (frozen pre-0.2.0 semantics — a bucket sharing a metered table's name is never counted into that meter by accident). To bill a bucket, opt in explicitly with { "resource": "storage", "table": ["media"] } in the meter's filters; resource also combines with op.
  • Avoid bucket names that collide with storage endpoint keywords (public, sign, authenticated, info, list, list-v2, upload, copy, move): such buckets mis-parse and may surface an object-path segment as the bucket name instead.

0.1.0

  • Initial release.

Links

License

MIT