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

@certnode/silent-write-audit

v0.3.1

Published

Static-analysis audit for phantom-column / phantom-field references in Supabase/PostgREST writes and Prisma client calls. Catches the silent-fail bug class where .update({...}) with a non-existent column drops the entire statement without error.

Readme

silent-write-audit

~90-second walkthrough (AI-narrated): bug class, what we found in our own codebase, what the audit delivers, pricing.

A static-analysis audit for the silent-fail bug class in Supabase / PostgREST writes — where a .update({...}) call with a column that doesn't exist on the target table is silently rejected, the entire statement is dropped, and your code logs one line nobody reads.

await supabase.from('disputes').update({
  status: 'won',
  won: true,                   // ← phantom column. PostgREST drops the entire UPDATE.
  evidence_score: 87,          // ← also phantom. You don't find out for weeks.
}).eq('id', disputeId)

PostgREST returns PGRST204. Your code gets { data: null, error: { ... } }, fires one console.error, and continues as if the write succeeded. Three weeks later you discover the entire feature was a no-op. This is the bug.

The bug class

PostgREST silently rejects the entire query if any column referenced in the payload, the .select() projection, or a filter chain (.eq(), .neq(), .gt(), etc.) doesn't exist on the target table. There is no partial-success path. Your code looks fine, the wire response is a 4xx, your logs have one line, and the rest of your application proceeds as if the write succeeded.

We pointed this audit at our own production codebase (CertNode — ~80k lines of TypeScript, Stripe + Supabase + Postgres) and it found ~50 silent-write bugs in one night. The most damaging ones:

  • Stripe dispute outcomes that hadn't been persisting for weeks (disputes.won and disputes.evidence_score were both phantom columns)
  • connected_accounts.charges_enabled never flipping after a Stripe deauthorization (phantom column on the deauth handler)
  • A GDPR redaction handler that was a silent no-op (phantom customer_email and dispute_evidence.organization_id)
  • Bitcoin Layer 3 timestamp verification 100% broken for 5+ weeks while the hourly cron returned success: true every run — three cascading silent bugs each masked by the next; only caught by an end-to-end verification check, never by unit tests or production logs
  • WooCommerce billing rows that never recorded fees (5 phantom columns in one row)

The audit catches all of these with static analysis. The pre-commit hook variant blocks new ones from shipping.

Install

npm install -D @certnode/silent-write-audit
# or run ad-hoc
npx @certnode/silent-write-audit --help

Quick start

# 1. Set Supabase Management API credentials
export SUPABASE_ACCESS_TOKEN=sbp_xxxxx     # https://supabase.com/dashboard/account/tokens
export SUPABASE_PROJECT_REF=xxxxxxxxxxxxxxx # in your project URL or Settings → General

# 2. Run the audit (scans ./app and ./lib by default)
npx silent-write-audit

# 3. (Optional) Run only on staged files for a pre-commit hook
npx silent-write-audit --staged --ci-critical-only

The first run hits the Supabase Management API for your live schema and caches it for 24 hours in .schema-cache.json. Subsequent runs are fast (a few hundred milliseconds for a mid-sized codebase).

Sample output

Run against a real codebase, the human-readable output looks like this (these are paraphrased real findings from our own production scan):

$ npx silent-write-audit

[silent-write-audit] Schema loaded: 47 tables
[silent-write-audit] Scanning 287 files...

[silent-write-audit] ⚠ Found 12 issue(s) (8 critical, 4 high)

🔥 CRITICAL  app/api/webhooks/stripe/dispute/route.ts:142
         table: disputes.update
         missing: won, evidence_score
         (PostgREST PGRST204 rejects the entire update — silent fail)

🔥 CRITICAL  app/api/webhooks/stripe/connected/route.ts:67
         table: connected_accounts.update
         missing: charges_enabled
         (PostgREST PGRST204 rejects the entire update — silent fail)

🔥 CRITICAL  lib/billing/woocommerce-fees.ts:198
         table: pending_fees.upsert
         missing: platform, external_dispute_id, fee_amount, fee_rate, amount_won
         (PostgREST PGRST204 rejects the entire upsert — silent fail)

🔥 CRITICAL  app/api/webhooks/shopify/gdpr/redact/route.ts:54
         table: dispute_evidence.delete (filter chain)
         missing: organization_id
         (PostgREST 400s the entire query — returns null data, silent fail)

⚠️  HIGH     lib/disputes/evidence-vault.ts:203
         table: dispute_evidence.select
         missing: organization_id
         (PostgREST 400s the entire query — returns null data, silent fail)

... (7 more findings omitted)

Tip: add `// silent-write-audit-ignore` on the line above an op to allowlist it.

Each 🔥 CRITICAL is a write or read against a revenue-tagged table that PostgREST has been silently rejecting. Each ⚠️ HIGH is a non-revenue table with the same bug class. The fix is one keystroke per finding (rename, remove, or move-to-metadata); the audit cost is the months of revenue you lost not knowing.

Output above is shape-illustrative: our own production scan found ~50 findings; we've shown 5 of the most damaging and abbreviated the rest. By default (no .silent-write-audit.json), all findings show as ⚠️ HIGH severity — 🔥 CRITICAL requires the optional criticalTables config below.

JSON output (--json) emits the same findings in a structured form for CI:

{
  "findings": [
    {
      "file": "app/api/webhooks/stripe/dispute/route.ts",
      "line": 142,
      "table": "disputes",
      "op": "update",
      "kind": "update_payload_phantom",
      "missing": ["won", "evidence_score"],
      "all_keys": ["status", "won", "evidence_score", "updated_at"],
      "severity": "critical"
    }
  ],
  "summary": { "total": 12, "critical": 8, "high": 4 }
}

Config

Optional .silent-write-audit.json in your project root:

{
  "scanDirs": ["app", "lib", "src/api"],
  "criticalTables": ["billing", "payments", "subscriptions", "disputes"]
}
  • scanDirs — directories to walk. Defaults to ["app", "lib"].
  • criticalTables — tables flagged with critical severity. Useful if you want a pre-commit hook that only blocks on revenue-critical changes (use --ci-critical-only). Default is empty (all findings are high).

CLI flags override the config file. Run silent-write-audit --help for the full list.

Allowlist

Add // silent-write-audit-ignore on the line above (or on the same line as) an operation to suppress findings for that op. Useful when you have a deliberate dynamic payload, a known phantom that's actually correct (e.g., a column that exists on a different schema), or a transitional migration:

// silent-write-audit-ignore  -- migration 0042 lands tomorrow
await supabase.from('users').update({ new_field: x }).eq('id', userId)

Pre-commit hook

There is an example pre-commit hook in examples/pre-commit. It runs the audit only on staged TypeScript changes and only fails on critical findings (so noise in dead-code paths doesn't block your commits).

cp examples/pre-commit .githooks/pre-commit
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks

This is what we run on the CertNode monorepo. It has blocked silent-write bugs from shipping on every commit since 2026-05-05.

What it covers (and what it doesn't)

Covers:

  • .update({...}), .upsert({...}), .insert({...}) payload keys
  • .select('col1, col2, joined(*)') top-level projections
  • All PostgREST filter methods that take a column-name as their first arg: eq, neq, gt, gte, lt, lte, like, ilike, is, in, contains, containedBy, range*, overlaps, textSearch, match
  • JSONB path expressions (metadata->>field resolves to base column metadata)
  • TypeScript and TSX (uses the TypeScript compiler API, not regex)

Does NOT cover (yet):

  • Drizzle, TypeORM, or other ORM patterns. (Prisma is supported as of v0.3; see "Prisma adapter" below. If you need Drizzle / TypeORM, open an issue.)
  • Spread operators in payloads ({...obj}). Can't be statically analyzed; will silently skip these objects.
  • Joined-relation column filters (.eq('related_table.col', value)). Skipped to avoid false positives, since validation requires parsing the chain's .select() to know which embeds were declared.
  • PostgreSQL direct connections. A future release will support DATABASE_URL and pg_dump schema files (gated on first non-Supabase user requesting it).

Prisma adapter (v0.3+)

If your codebase uses Prisma instead of (or alongside) Supabase-JS, the audit can validate Prisma call sites against your schema.prisma:

# Default: looks for ./prisma/schema.prisma
npx silent-write-audit --skip-supabase

# Or point at a custom path (monorepo case)
npx silent-write-audit --skip-supabase --prisma-schema packages/db/schema.prisma

# Both Supabase + Prisma in the same repo (rare but supported)
npx silent-write-audit  # auto-detects both

What's checked:

  • prisma.<model>.create({ data: { ... } }) — payload keys vs schema fields
  • prisma.<model>.update({ where, data }) — both halves checked independently
  • prisma.<model>.upsert({ where, create, update }) — three sections each checked
  • prisma.<model>.findUnique / findFirst / findMany({ where, select }) — both where keys and select keys
  • prisma.<model>.delete / deleteMany / updateMany / count / aggregate({ where })where keys
  • prisma.<model>.createMany({ data: [...] }) — first element of array (representative shape)

Compound-unique-key support. Prisma generates synthetic where-clause fields from @@unique([a, b]) and @@id([a, b]) directives. The adapter recognizes:

  • @@unique([a, b]) → synthetic a_b
  • @@unique([a, b], name: "myKey") → synthetic myKey
  • @@unique(fields: [a, b]) → synthetic a_b
  • @@id([a, b]) → synthetic a_b

These are valid where inputs and are NOT flagged as phantom.

Multi-schema monorepos. If your scan paths contain other schema.prisma files (e.g., packages/platform/examples/base/prisma/schema.prisma), the adapter auto-detects them and skips Prisma checks under those subtrees — those files use a different schema. The active schema specified by --prisma-schema is the only one validated against; sub-schema dirs are reported on startup so you can confirm the boundaries.

Custom client names. By default the adapter recognizes prisma, db, tx, and client as Prisma client identifiers. Override via config:

{
  "prismaClientNames": ["prisma", "db", "tx", "extendedDb"]
}

Flags:

  • --prisma-schema PATH — path to schema.prisma (default: ./prisma/schema.prisma)
  • --skip-supabase — skip Supabase scan (Prisma-only mode)
  • --skip-prisma — skip Prisma scan (Supabase-only mode)

CI integration

JSON output for parsing in CI:

npx silent-write-audit --json > audit.json

Same shape as the JSON example in the Sample output section above. CI parsers can read summary.critical directly to gate builds on critical-only findings.

Exit codes:

  • 0 — no findings (or no critical findings if --ci-critical-only)
  • 1 — findings present
  • 2 — configuration / fatal error

Why this exists

The bug class is real and unforgiving — PostgREST gives you no recovery path, just a silent rejection. Most teams discover these bugs through outages: a dispute that didn't get marked won, a billing flag that didn't flip, a webhook that quietly stopped persisting. The fix in code is one keystroke; the audit cost is the months of revenue you lost not knowing.

We built this to fix our own codebase. We're open-sourcing it because the bug class is the same shape in every PostgREST shop on Earth.

⸻ Audit-as-a-service

If you'd rather have someone external run this on your repo, triage the findings, and ship patch PRs — we offer that as a fixed-price service.

| Tier | Price | What you get | Best for | |---|---|---|---| | Findings | $497 one-time | Audit run + ranked fix list (impact-scored). 3-day turnaround. | Teams with bandwidth to fix the bugs themselves | | Full delivery | $1,497 one-time | Everything in Findings + patch PRs for top 10 + pre-commit hook installed and configured. 7-day turnaround. | Bandwidth-limited shops; the default | | Watch | $4,997 / year | Quarterly audit re-runs + new-bug alerts as schema evolves + pre-commit hook maintained. | Teams that want ongoing protection past the first audit |

No charge if we find fewer than 3 critical findings — if your codebase is mostly clean, congrats, you owe nothing. (Applies to Findings and Full delivery tiers. "Critical" = phantom column on a revenue-tagged table per your .silent-write-audit.json config.)

First 5 customers (Full delivery only): $1,197 with code FIRST5 (a $300 founder's discount).

Two ways to engage:

  • Self-serve (FIRST5): pay $1,197 via Stripe — we'll reach out within 24h to collect your schema-dump credential and start the audit.
  • Email first: drop a note to [email protected] with subject "silent-write audit — [your company]". Reply within 24h. Include stack confirmation (Postgres? Supabase? Stripe?), repo size estimate, and how to get a read-only schema dump (Supabase Management token, pg_dump URL, or a one-time schema file). Invoice on day 7 after PRs are merged or rejected.

This is run by the CertNode team — Stripe Partner (Apps track), 4 published Stripe Marketplace apps, primary author of the audit you just installed.

What Watch delivers in detail

Watch is the recurring-protection tier. Concrete deliverables for $4,997/year:

  • Quarterly audit re-runs — 4× per year against your live schema. We catch new phantom-column references that appeared since the last audit.
  • Pre-commit webhook integration — every finding caught at commit time POSTs to your Slack / Discord / custom URL. See examples/pre-commit-with-watch-webhook for the install. Configure with WATCH_WEBHOOK_URL (and optional WATCH_WEBHOOK_SECRET for HMAC-signed payloads).
  • Priority 24h response on new findings (vs the 7-day SLA on one-time tiers).
  • New-bug-class release notes — when the audit logic gains new patterns (new ORM adapter, new column types, sibling tools we ship), Watch subscribers get a release-note email rather than discovering it on next quarterly run.
  • Annual aggregated trend report — findings-over-time chart you can hand to your auditor or board (compliance-useful for SOC2 / ISO 27001 prep).
  • Pre-commit hook maintenance — when you change Postgres provider, add tables, or evolve schema patterns, we update your .silent-write-audit.json config so the gate stays accurate without you maintaining it.

Watch can be cancelled at any time; we don't lock you into the year. Pro-rated refund on remaining quarters if you cancel mid-cycle.

Audited by CertNode

If you've engaged the audit (paid or via the OSS tool) and your codebase is clean (or post-fix), you can publicly attest. We don't gate the badge — running the audit makes it yours:

[![Audited by CertNode](https://img.shields.io/badge/Audited_by-CertNode-10b981)](https://github.com/srbryant86/silent-write-audit)

Renders as a small green pill in your README. Or a tweet template:

"Our codebase was audited for silent-write bugs by CertNode. [N] findings caught + fixed. The audit tool is open source: github.com/srbryant86/silent-write-audit"

The badge isn't a certification — it's a public commitment to the bug class. The audit is reproducible by anyone with npx.

Examples

  • examples/pre-commit — basic pre-commit hook that blocks commits on critical findings.
  • examples/pre-commit-with-watch-webhook — extended pre-commit for Watch-tier customers; same blocking behavior plus webhook POST on every finding.
  • examples/sample-report.md — what a Full-delivery audit deliverable looks like. Real-shape with illustrative customer name; structure matches what paid customers receive.
  • examples/audit-deliverable-template.md — the internal template auditors fill in. Published so customers can preview the deliverable structure before engaging.
  • examples/auto-pr-template.md — patch-PR description format used in Full-delivery engagements. Lifts per-PR write-time from ~30min → ~5min and surfaces the revenue impact of each fix consistently across PRs.

Roadmap

Each release below is demand-gated — built when a real user (not us) asks for it. We'd rather ship narrow on real signal than broad on speculation.

  • v0.4 — Drizzle adapter. Different parser path. Gated on first Drizzle user opening an issue with a representative file.
  • v0.5 — DATABASE_URL + pg_dump schema sources. Drops the Supabase Management API requirement; works on any Postgres + PostgREST stack via direct connection or schema file.
  • v0.6 — rpc() parameter audit. Stored-procedure call sites cross-referenced against pg_proc definitions.
  • Sibling tools (separate packages): stripe-webhook-audit (idempotency + signing + retry), supabase-rls-audit (missing RLS on public tables), stripe-api-version-audit (deprecated patterns). Each independently useful, same shape as this audit.

Already shipped: published to npm as @certnode/silent-write-audit (latest v0.3.0). Install via npm install -D @certnode/silent-write-audit or run ad-hoc with npx. v0.3 adds the Prisma adapter.

Contributing

Bug reports and PRs welcome. Please include a minimal reproducible example showing the bug class your patch addresses or fixes.

License

MIT — see LICENSE.

Author

Built at CertNode. Contact at [email protected].