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

quackhole

v0.0.3

Published

Query a DuckDB behind NAT from a browser or another DuckDB, over an encrypted peer-to-peer connection

Readme

quackhole

Query a DuckDB that has no address — a laptop on cafe Wi-Fi, a server behind a corporate NAT, a home server with no public IP — from a browser or from another DuckDB. No port forward, no VPN, no certificates.

Two front doors, one package:

npx quackhole          on the machine you want to reach; prints a link
import * as quackhole from 'https://cdn.jsdelivr.net/npm/quackhole@0/dist/quackhole.js';

The address is a 32-byte ed25519 public key, so connecting to the right machine and authenticating it are the same operation. Quack stays the database protocol; quackhole carries its HTTP over iroh QUIC streams.


Read this before the browser half

The page must be cross-origin isolated, and that can only come from the page's own origin. DuckDB-Wasm's Quack client issues a synchronous XHR, so the DuckDB thread blocks in Atomics.wait, which needs a SharedArrayBuffer, which needs COOP/COEP. There is no async fallback short of patching duckdb-wasm.

Serve your page with:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

On a host that will not set headers — GitHub Pages, most static CDNs — copy coi-serviceworker.js to your own origin and load it before anything else. It cannot come from a CDN: a service worker can only be registered from the origin that served it, and it registers itself by document.currentScript.src, so a copy under assets/ scopes the worker to assets/ and silently stops it controlling the page. Put it at your root.

Get this wrong and the failure is the least legible one there is: the worker is fetched, is 200, and dies with an error event carrying no message. So ask first:

const { ok, reason } = quackhole.check();
if (!ok) console.error(reason);   // names the problem and the fix

createWorker throws the same message rather than starting a worker that cannot work.

The browser client

createWorker is the whole integration. duckdb-wasm exports a createWorker of its own, so this reads as a one-identifier swap; everything after it is unmodified duckdb-wasm.

import * as duckdb from '@duckdb/duckdb-wasm';
import * as quackhole from 'https://cdn.jsdelivr.net/npm/quackhole@0/dist/quackhole.js';

// The ticket `npx quackhole` printed, carried in the link's fragment -- which
// never leaves the browser.
const ticket = decodeURIComponent(location.hash.slice(1));

const bundle = await duckdb.selectBundle(duckdb.getJsDelivrBundles());
const worker = await quackhole.createWorker(bundle.mainWorker);
const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(duckdb.LogLevel.WARNING), worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);

const conn = await db.connect();
await conn.query('INSTALL quack');
await conn.query('LOAD quack');

// One ticket in, one catalog out: the named secret, its scope, the peer's relay
// and the ATTACH. Holding several remotes at once is what the session is for.
const session = new quackhole.QuackholeSession({ conn, worker });
const remote = await session.attach(ticket);

// A Quack catalog enumerates nothing -- duckdb_tables() and information_schema
// are both empty for it -- so this is the one listing it pushes to the remote.
const tables = await session.tables();
console.log(`${remote.name} in ${Math.round(remote.attachMs)}ms:`, tables.get(remote.name));

Then it is ordinary SQL: SELECT * FROM remote.events, joins against local tables, whatever DuckDB does.

Pin the file path, and do not use jsDelivr's /+esm. That transform bundles a package into a single module, and this one must not be: quackhole.js derives its base from import.meta.url and then fetches web/qh-worker.js as a real sibling over the network. Under +esm there is no sibling to fetch, and the failure arrives as a 404 inside a worker.

What it exports

| | | |---|---| | createWorker(mainWorker, options?) | A DuckDB-Wasm worker with the transport installed. Pass it to AsyncDuckDB | | QuackholeSession({ conn, worker }) | Attach, detach, list connections, keep the list honest against DuckDB | | parseTicket(ticket) | Endpoint id, relay, token, address and secret name, read by the same Rust that mints them | | check() | { ok, reason } — whether this page can run the transport |

options are the settings web/README.md documents: mode, relay, relays, timeout, chunk, intercept, debug. You need none of them for one remote.

relay and relays are different axes, and neither is needed by default. A ticket already carries the relay that reaches its peer; relays is which relays this page's own endpoint homes on, for a deployment that wants none of its traffic on n0's public relays.

Vendoring it instead

The client is six files and a wasm/ directory under dist/web/, copied here verbatim and never bundled. Serve them from your own origin and point a worker at dist/web/qh-worker.js?target=<duckdb worker url>&mode=iroh — no loader needed, because it reads its settings off its own URL when it has one.

npx quackhole

On the machine you want to reach:

npx quackhole

It downloads the extension for your platform (cached after the first run), builds a small sample database in a temp directory, starts serving over iroh, and prints a link that opens the browser workbench already connecting. Nothing is installed, no key is persisted, and nothing is left running after Ctrl-C.

| | | |---|---| | --token <t> | Use a token of your own rather than a fresh random one | | --port <n> | Quack's local port, if 9494 is taken | | --page <url> | Aim the printed link at another workbench | | --relay <url> | Home on an iroh relay you run instead of n0's. Repeatable | | QH_EXT=<path> | Use a locally built extension, for developing on the repo |

--relay is the whole of the change: the ticket carries whichever relay the endpoint homed on, so the browser follows without being configured.

The link carries the token. Treat it like one: anyone holding it can query that database until you stop.

What this cannot do

  • A browser can only be a client. quack_serve throws NotImplementedException on wasm.
  • A browser can only relay. iroh compiles its IP transport out under cfg(wasm_browser), because a browser cannot open a UDP socket. There is no hole punching and no direct path from a tab, ever. Traffic stays end-to-end encrypted — the relay forwards ciphertext it cannot read.
  • No isolation, no transport. See the first section. There is no fallback.

Source

github.com/smithclay/quackhole — the DuckDB extension, the shared Rust core both clients link, and the test harness.

MIT.