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

@boldsec/supabase

v0.1.0

Published

BoLD live authorization monitoring for Supabase: a fetch-level wrapper for supabase-js watches your PostgREST/RPC traffic for cross-user access (BOLA/IDOR) and mass-assignment (BOPLA) risks. Metadata only, fail-safe, zero query-builder patching.

Readme

@boldsec/supabase

Live BOLA/IDOR + mass-assignment (BOPLA) monitoring for Supabase. Install one fetch wrapper at the createClient({ global: { fetch } } ) boundary; BoLD watches your real PostgREST/RPC traffic and raises an alarm the moment one user reaches another user's row, in real time, from your app's actual traffic, no query-builder patching, no schema introspection, no dependency on @supabase/supabase-js itself.

  • Metadata only. BoLD receives who made the request (the Supabase Auth sub claim, decoded from the request's own bearer JWT, never the token itself), the canonical endpoint shape (/rest/v1/<table>/{id}), the object id, the status, and the object's declared owner column if the response exposed one. The response row is read once to extract that single owner field and then discarded. No body, no credentials, and no Supabase service_role/anon key ever leave your app.
  • Fail-safe. The real Supabase call always runs first and its response is always returned completely untouched. Shipping the metadata can never throw back to your app or corrupt its response. It DOES add one more network round trip, to BoLD's own ingest endpoint, on a call BoLD observes: the wrapper AWAITS that shipment (a deliberate choice, not an oversight) so the metadata is not silently dropped on a serverless/edge runtime that freezes the moment a response returns. A down or slow ingest endpoint still never breaks your app; it can only ever add latency to the observed call, never fail it.
  • Never patches the query builder. wrapSupabaseFetch is a plain fetch-compatible function, installed via supabase-js's own documented global.fetch option, a stable, public escape hatch, not an undocumented internal.

Install

npm install @boldsec/supabase

Works with @supabase/supabase-js v2 (peer, not a dependency of this package, there is none).

Configure (once)

Add your keys from the BoLD Live page (shown once when you connect the app):

BOLD_INGEST_URL=https://<your-bold>/api/live/ingest
BOLD_INGEST_KEY=blk_...
# optional: owner-field names to look for in responses (default covers common ones, incl. owner_id)
BOLD_OWNER_FIELDS=ownerId,owner_id,user_id
# optional: your primary-key column name, if it is not "id"
BOLD_SUPABASE_ID_COLUMN=id

Use

Wrap the fetch you hand to createClient, on a server-side Supabase client only (see the browser-runtime note below):

import { createClient } from "@supabase/supabase-js";
import { wrapSupabaseFetch } from "@boldsec/supabase";

const supabase = createClient(supabaseUrl, supabaseKey, {
  global: { fetch: wrapSupabaseFetch() },
});

Unconfigured (no BOLD_INGEST_URL), the wrapper is a transparent pass-through, so it is safe to leave wired in every environment.

SERVER-SIDE ONLY (a load-bearing constraint, not a suggestion)

BOLD_INGEST_KEY is BoLD's own ingest credential and must never ship inside a browser bundle. Every other BoLD adapter is structurally server-only (Express/Fastify/Koa/Hono/Nest middleware, a Next.js route handler); supabase-js is unusually often instantiated directly in the browser (Supabase's own "no backend required" pattern). wrapSupabaseFetch defends against this proactively: it refuses to observe at all when it detects a runtime it must not ship a secret from, a main-thread browser tab, a Web Worker (dedicated, shared, or service worker), or, as a broader third signal, the absence of a Node-like process.versions.node. The real Supabase call is completely unaffected either way, only BoLD's own observation is skipped. Wire this into a server-side Supabase client (a Route Handler, a Server Component, a Node backend). A purely client-side/browser/worker Supabase integration is not observed by this connector in this phase.

The runtime guard blocks the SHIP, not the BUNDLE. This is a real distinction, not a nuance to skim past: the guard stops BoLD's own code from ever making the network call that would send BOLD_INGEST_KEY anywhere, at runtime. It does nothing to keep the KEY ITSELF out of a browser bundle's compiled source in the first place, that is a build-time concern, entirely on the operator:

  • Never pass ingestKey as a literal string inline in a module that a bundler (webpack, Vite, Next's client compiler) can reach from client-side code, even behind an if (typeof window === "undefined") guard in your OWN code, a bundler can still statically include the literal string in the shipped file; only code that is NEVER imported by a client entry point is safe.
  • Never expose BOLD_INGEST_KEY through a "public"/inlined build-time env var convention (NEXT_PUBLIC_BOLD_INGEST_KEY, VITE_BOLD_INGEST_KEY, or similar). Those conventions exist specifically to BAKE a value into the client bundle at build time; using one for this key defeats the runtime guard entirely, since the secret is already sitting in plain text in shipped JS before the guard ever runs. Set BOLD_INGEST_KEY as an ordinary, server-only environment variable, read only from a module your server entry points import, never a client one.

Use the caller's own per-user, RLS-enforcing session, never a service_role client. Wrap a Supabase client authenticated as the actual signed-in user (their forwarded access token), not one authenticated with the service_role key. Wrapping a service_role client is both an anthem risk and a signal-quality problem, not merely a style preference:

  • service_role bypasses Row Level Security entirely, so every read this connector observes through it "succeeds" regardless of who owns the row, RLS is not actually being exercised on that traffic at all, and any cross-user comparison drawn from it says nothing true about your app's real enforcement.
  • service_role's JWT carries no per-user sub claim, it identifies the SERVICE, not a caller. Every request wrapped through it decodes to the SAME caller identity (or falls back to the same constant hash, since the bearer string never changes between requests), so the connector can no longer tell one real user's access from another's at all. The result is needs-review noise, not a usable signal, never a meaningful CONFIRMED or a meaningful clean, for any traffic sent through that client.

What BoLD answers

One question: can one user reach another user's row? BoLD is a runtime alarm for this family (BOLA/IDOR, function-level BFLA, mass-assignment BOPLA, tenant isolation), never a general scanner. A Supabase cross-user row read is judged by the exact same deterministic decision logic every other BoLD connector uses.

What this connector sees, and what it does not (honest ceilings)

  • Table reads/writes (.select() / .update() / .delete(), and a create only when it also carries an id-equality filter) are observed only when the request's own query string proves it targeted exactly one object, an <id_column>=eq.<value> filter (default id). Any other filter operator, no filter at all, an or=(...) combinator, or a filter matching more than one row all ship nothing, BoLD never guesses which row a list-shaped or ambiguous request touched.
  • A bare .insert() (a table create with no .eq()/.match()) is never observed in this phase. Supabase creates carry no query-string filter to gate on, so this connector cannot yet attribute a create to a single object. Mass-assignment on a create specifically (e.g. a self-registration setting its own role) is not caught by this passive wrapper today; .update() writes (the common pattern for an existing object) are fully covered.
  • A write whose response has no readable body (the PostgREST default 204 No Content a plain .delete()/.update() gets without Prefer: return=representation) is not observed, there is no row to read an id/owner from. Chain .select() after the write (the idiomatic way to opt into return=representation) and it is observed.
  • RPC (.rpc()) coverage is narrow, by design. Only an RPC whose response is a single, row-shaped JSON object, with both a real (non-null) id AND a real, extractable owner value, is observed. A setof-returning function's array response, a scalar-returning function's bare value, and (the recorded, load-bearing case) a returns rowtype function's all-null composite on a zero-row/ RLS-blocked result all ship nothing. RPC parameters are never inspected for id/owner/BOPLA purposes, only the response.
  • Storage (supabase.storage) ships nothing in this phase. A list() call's metadata array carries no per-object owner field in Supabase's default schema; a download()'s raw bytes are never parsed.
  • /auth/v1/... is never inspected, beyond decoding the sub claim off the request's own bearer JWT for caller identity (no signature verification, Supabase's own gateway already verified it to produce the response at all). A password, an OTP, or a refresh token is never read or forwarded.
  • This connector only sees calls made through the Supabase-JS client instance it wraps. A direct PostgREST HTTP call from another client/language, a call from a different backend service using its own Supabase client, and the native mobile SDKs (Swift/Kotlin/Flutter) are not observed. This is a real, named coverage boundary ("wrapped calls only"), never folded into a generic all-clear.

The locked claim, everywhere this connector's findings are surfaced: "verify live cross-user access," never "verify your RLS policy." BoLD proves what actually happened over the wire to a real request, a different, stronger claim than a static policy audit, which is Supabase's own Advisor's job.

Development

  • npm test, unit tests (src/index.test.ts) plus the recorded-ground-truth fixture suite (src/ground-truth.test.ts, byte-for-byte from ground-truth/wire-behavior.md and ground-truth/RPC_STORAGE_BEHAVIOR.md).
  • node conformance.mjs, the Supabase-only golden-vector conformance gate (../bold-conformance/test-vectors-supabase.json).
  • npm run test:live, an optional, NOT-CI-wired end-to-end test against a real local Supabase CLI stack (supabase start), for local development only.