From the browser
The n6k extension is compiled to WASM as well as native, so a browser tab is a first-class client. It attaches a catalog, runs SQL against it in the tab, and gets Arrow back over one WebSocket.
This is not a separate product from the rest of the site — it is how every
page here works. A data.py publishes tables, the server serves them as a
catalog, and the page's DuckDB-WASM attaches that catalog and queries it. The
charts on the showcase pages are SELECT statements running a few
inches from your eyes.
The default: the driver owns the socket
Same statement as the CLI. In the browser the token comes from the host application rather than a persistent secret, since there is nowhere durable to keep it:
await conn.query(
`ATTACH 'n6ks://data.example' AS db (TYPE n6k, token '${jwt}')`,
);
await conn.query(`SELECT region, sum(amount) FROM db.orders GROUP BY 1`);
Riding a socket the app already opened
An application that already holds a WebSocket — for auth, for reconnection, for its own messages — can hand it to the extension instead of having a second one opened alongside it:
const { conn, registerWebsocket, replaceWebsocket } = await createDuckDB();
const wsId = registerWebsocket(ws); // returns "ext_1"
await conn.query(`ATTACH '' AS db (TYPE n6k, wsId '${wsId}')`);
The URL is empty because there is nothing left to dial. Three things then become the application's job:
- Catalog binding — from
?catalog=<name>on the socket's URL, or, if the URL omits it, from the alias the driver declares in its handshake. If both are present and disagree, the server rejects the attach. - Auth — whatever the upgrade carried (a cookie, an
Authorizationheader, a query parameter) already authenticated the socket. - Reconnection — the driver cannot reopen a socket it does not own. On a
drop it reports
disconnected; the app opens a fresh socket and callsreplaceWebsocket(wsId, newWs). The catalog stays attached, so there is noDETACH/ATTACHcycle — only the in-flight requests fail, and the app re-issues them.
Sharing one socket with your own traffic
Every n6k frame is a binary WebSocket message. The driver claims binary
frames and leaves text alone, attaching its listener with
addEventListener rather than overwriting onmessage — so the application
keeps sending and receiving its own text messages on the same socket.
Two conditions apply. Don't put your own binary traffic on a shared socket
(binaryType is forced to "arraybuffer"), and your server must demultiplex
the mirror image: route inbound binary frames into an n6k connection and keep
your text handling separate. A socket dedicated to n6k needs no server change at
all — point it at /ws?catalog=<name>.
One registered socket backs one catalog, matching the server's model. Use a separate socket per catalog.
The native equivalent
A native host process can do the same thing one level down, handing the
extension a connected socket fd — typically from socket.socketpair() — and
pumping frames between it and a WebSocket the host opened itself:
ATTACH '' AS db (TYPE n6k, wsFd 7);
wsId is browser-only and wsFd is native-only; each throws on the other
build. The fd path adds one requirement the WebSocket path gets for free: a raw
byte stream has no message boundaries, so every frame must be written as
[4-byte big-endian length][frame bytes].
Connection readiness
A server session can take seconds to build — attaching a remote source, warming a catalog. The driver separates transport up from session usable:
ATTACH returns as soon as the WebSocket handshake completes. It creates only
the default schema, with no round trip. The first catalog access is what
waits for the session, bounded by ready_timeout (default 60s):
ATTACH 'n6ks://data.example' AS db (TYPE n6k, ready_timeout '120s');
Exceed it and the first query fails with a typed n6k session not ready after …
rather than a bare socket error. A build that fails surfaces the server's own
error on that first query.
ready_timeout is honored on native only. The WASM build gates the first
request the same way, but surfaces a stuck or failed build by closing the
socket rather than by counting down a timer.