The official browser-only QuestDB client. It provides QWP ingestion, streaming queries, failover, typed row writers, and browser session authentication without Node.js modules or polyfills.
The complete browser API is exported from @questdb/browser-client. There are
no additional public import paths.
ws, or undiciWebSocket, fetch, URL, TextEncoder, and
TextDecoder/write/v4 and /read/v1/exec REST route when authentication bootstrap is neededThis package does not contain the Node.js ILP transports; use
@questdb/nodejs-client for server-side Node.js programs.
npm install @questdb/browser-client
yarn add @questdb/browser-client
pnpm add @questdb/browser-client
The package works with browser bundlers such as Vite, Rollup, webpack, and esbuild. Import only from the package root:
import { connectQwpBrowserSender } from "@questdb/browser-client";
Serve QuestDB's QWP route from the application's origin, either directly or through a reverse proxy. The browser will then apply the page's normal cookie, origin, and TLS rules to the WebSocket connection. An application served from another origin connects directly instead; see Third-party web applications.
import { connectQwpBrowserSender } from "@questdb/browser-client";
const writeUrl = new URL("/write/v4", window.location.href);
writeUrl.protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const sender = await connectQwpBrowserSender(
{ url: writeUrl },
{ autoFlush: false },
);
try {
await sender
.table("page_events")
.symbol("kind", "view")
.stringColumn("path", window.location.pathname)
.timestampColumn("recorded_at", Date.now(), "ms")
.atNow();
await sender.flush();
} finally {
await sender.close();
}
Use wss: whenever the page is served over HTTPS. Browsers block insecure
WebSockets from secure pages.
Transactional mode keeps automatically emitted frames in one open server-side
transaction. commit() publishes the final frame. Transactions are atomic per
table, not across every table in one flush.
import { connectQwpBrowserSender } from "@questdb/browser-client";
const sender = await connectQwpBrowserSender(
{ url: writeUrl, requestDurableAck: true },
{
transactional: true,
autoFlushRows: 10_000,
awaitDurableAck: true,
durableAckTimeoutMs: 30_000,
},
);
try {
for (const event of [
{ source: "checkout", value: 1n, timestamp: Date.now() },
{ source: "search", value: 3n, timestamp: Date.now() },
]) {
await sender
.table("events")
.symbol("source", event.source)
.longColumn("value", event.value)
.at(event.timestamp, "ms");
}
await sender.commit();
} finally {
await sender.close();
}
Browser replay is held in memory and survives reconnects only while the page is alive. Persistent store-and-forward is intentionally available only from the Node.js package.
Compile a table schema once when application data already has an object shape. TypeScript checks every row against the schema.
import {
connectQwpBrowserSender,
designatedTimestamp,
double,
symbol,
} from "@questdb/browser-client";
const sender = await connectQwpBrowserSender({ url: writeUrl });
try {
const measurements = sender.writer("measurements", {
device: symbol(),
temperature: double(),
timestamp: designatedTimestamp("ms"),
});
await measurements.rows([
{ device: "sensor-1", temperature: 21.4, timestamp: Date.now() },
{ device: "sensor-2", temperature: 22.1, timestamp: Date.now() },
]);
await sender.flush();
} finally {
await sender.close();
}
The schema vocabulary also covers QuestDB integers, decimals, UUIDs, IPv4 addresses, geohashes, binary values, and arrays.
Browser JavaScript cannot add an Authorization header to a WebSocket upgrade,
so the client authenticates in one of two ways:
auth, for a third-party web application served from its own origin.
The client sends the credential with every upgrade, in its subprotocol offer.A connection accepts one or the other, not both.
Authenticate over REST first so QuestDB can set an HttpOnly session cookie. The browser then sends that cookie during the QWP WebSocket upgrade.
import {
bootstrapQwpBrowserSession,
connectQwpBrowserSender,
} from "@questdb/browser-client";
await bootstrapQwpBrowserSession({
url: new URL("/exec", window.location.href),
authentication: {
type: "bearer",
token: oidcOrRestAccessToken,
},
// QuestDB Enterprise only; omit to use the authenticated principal.
serviceAccount: "market_data_writer",
});
const sender = await connectQwpBrowserSender({ url: writeUrl });
Basic authentication is also supported:
const sender = await connectQwpBrowserSender({
url: writeUrl,
sessionBootstrap: {
authentication: {
type: "basic",
username: "admin",
password: "quest",
},
},
});
Putting sessionBootstrap on the connection options repeats authentication
before initial connection, reconnect, and failover attempts. The package does
not run an interactive OIDC flow; the application obtains access tokens from
its identity provider.
The bootstrap request uses credentials. Prefer serving /exec, /write/v4,
and /read/v1 from the application's origin. JavaScript never reads the
HttpOnly cookie. An application on another origin should use auth instead:
QuestDB ignores cookies on a cross-origin QWP upgrade.
An application served from its own origin, such as https://app.example.com,
connects to QuestDB directly once the server lists that exact origin:
# QuestDB server.conf
qwp.browser.allowed.origins=https://app.example.com
Pass the credential as auth. REST and OIDC tokens use the bearer form, and
{ type: "basic", username, password } is accepted too:
import { connectQwpBrowserSender } from "@questdb/browser-client";
const sender = await connectQwpBrowserSender({
url: "wss://questdb.example.com:9000/write/v4",
auth: { type: "bearer", token: restToken },
});
An OIDC access token expires, so pass a function instead. The client calls it before every initial connect, reconnect, and failover attempt, so a long-lived sender or query session keeps working with each refreshed token:
const sender = await connectQwpBrowserSender({
url: "wss://questdb.example.com:9000/write/v4",
auth: async ({ signal }) => ({
type: "bearer",
token: await identityProvider.getAccessToken({ signal }),
}),
});
The signal aborts when the connection attempt is abandoned, and time spent in
the function counts against connectTimeoutMs. If the function throws, the
attempt fails with a QwpUpgradeError of kind authentication whose cause is
the thrown error; reconnects retry it with backoff unless that error carries
retryable: false.
wss:. The credential travels in the upgrade request's
Sec-WebSocket-Protocol header, base64url-encoded but not encrypted.serviceAccount assumption is not
available this way.qwp.browser.allowed.origins. The allowlist covers
only the QWP routes; it does not enable CORS for REST.QWP egress streams typed result batches. A session runs one active query at a time and automatically reconnects and walks configured failover URLs.
import { connectQwpBrowserEgress } from "@questdb/browser-client";
const readUrl = new URL("/read/v1", window.location.href);
readUrl.protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const session = await connectQwpBrowserEgress(
{
url: readUrl,
compression: "zstd",
sessionBootstrap: {
authentication: { type: "bearer", token: oidcOrRestAccessToken },
},
},
{ queryTimeoutMs: 30_000 },
);
try {
const query = await session.query(
"select timestamp, device, temperature " +
"from measurements where device = $1",
{
// Bind index 0 corresponds to SQL placeholder $1.
binds: (binds) => binds.setVarchar(0, "sensor-1"),
// A positive credit window bounds server read-ahead.
initialCredit: 1024 * 1024,
},
);
for await (const batch of query) {
for (const row of batch.rows()) {
console.log(row);
}
}
await query.completion;
} finally {
await session.close();
}
Use queryViews() for reusable zero-copy result views in allocation-sensitive
applications. Copy any view that must outlive its batch callback.
connectQwpBrowserClient() creates bounded sender and query pools for an
application component that needs concurrent ingestion and queries:
import { connectQwpBrowserClient } from "@questdb/browser-client";
const clusterUrl = new URL("/", window.location.href);
clusterUrl.protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const db = await connectQwpBrowserClient({
cluster: {
url: clusterUrl,
sessionBootstrap: {
authentication: { type: "bearer", token: oidcOrRestAccessToken },
},
},
ingress: { requestDurableAck: true },
egress: { target: "replica", compression: "zstd" },
pool: { senderPoolMax: 2, queryPoolMax: 4 },
});
try {
const sender = await db.borrowSender();
try {
await sender.table("events").symbol("kind", "view").atNow();
} finally {
// Flushes completed rows and returns the sender to the pool.
await sender.close();
}
const query = await db.borrowQuery();
try {
const result = await query.query("select count() from events");
for await (const batch of result) console.log([...batch.rows()]);
await result.completion;
} finally {
await query.close();
}
} finally {
await db.close();
}
finally blocks.at(), atNow(), and
writer row()/rows() calls.flush() or close().onReplayReset callback when duplicate prefixes matter.
Browser WebSocket adapter and browser-safe QWP protocol/session APIs.