JavaScript SDK

Query ScopeDB and write events from server-side JavaScript or TypeScript. This guide uses scopedb 0.3.0.

Install and configure

The examples use Node.js 20 or later. Install the SDK in your application:

npm install scopedb@0.3.0

The package includes TypeScript types and uses ES modules. The examples below run as .mjs files without a TypeScript runner. Bun and Cloudflare Workers are also supported; CommonJS applications can load the package with await import("scopedb") inside an async function.

Follow the application connection guide to obtain an endpoint and API key, then set them in the shell where you run the examples:

export SCOPEDB_ENDPOINT="https://<endpoint>"
export SCOPEDB_API_KEY="<api-key>"

Keep the key on your server. SDKs do not reuse the CLI's interactive login.

Run a query

Save this complete program as example.mjs and run node example.mjs. It verifies the connection without creating a table or requiring sample data.

import { Client } from "scopedb";

const endpoint = process.env.SCOPEDB_ENDPOINT;
const apiKey = process.env.SCOPEDB_API_KEY;
if (!endpoint || !apiKey) {
  throw new Error("SCOPEDB_ENDPOINT and SCOPEDB_API_KEY are required");
}

const client = new Client(endpoint, { apiKey });
const result = await client.query("SELECT 1 AS ready");
const rows = result.toObjects();
console.log("ready: " + rows[0].ready);

Expected output:

ready: 1

query() waits for execution to finish; toObjects() returns rows keyed by column name. Keep this client and add the following examples below the program.

Write rows

Create the events table used by the quickstart. This statement leaves an existing table unchanged; if you already have events, check that its columns match before continuing.

await client.query(`
  CREATE TABLE IF NOT EXISTS events (
    time timestamp,
    service string,
    name string,
    message string,
    var object
  )
`);

Append one event and wait for it to commit:

const table = client.table("events");
const row = {
  time: "2026-10-23T09:00:00Z",
  service: "checkout",
  name: "payment_failed",
  message: "Credit card gateway timed out",
  var: { region: "apac", gateway: "credit_card" },
};
const stream = table.appendStream().build();
await stream.send(row);
const delivered = await stream.shutdown();
console.log("Written rows: " + (delivered?.num_rows_inserted ?? 0));

send() accepts a row into the SDK's buffer. shutdown() sends remaining rows, waits for delivery, and closes the stream. With this default failure policy, a failed batch throws. A successful run prints Written rows: 1.

Read the event back after shutdown:

const written = await client.query(`
  FROM events
  WHERE time = '2026-10-23T09:00:00Z'::timestamp
    AND service = 'checkout'
    AND name = 'payment_failed'
  SELECT service, name, var['gateway']::string AS gateway
  LIMIT 10
`);
console.log(written.toObjects());

The result includes checkout, payment_failed, and credit_card. Each run appends another event. The examples use the default scopedb database and public schema; pass { database, schema } to client.table() for another table location.

Work with results

Use toObjects() for named columns or toValues() for positional rows. An empty result produces an empty array. first() returns one object or null; object conversion rejects duplicate column names, so use unique aliases or positional results.

Result value mapping

toObjects(), toValues(), and first() use these conversions:

ScopeDB result typeJavaScript valueNotes
intbigintUse integerMode to return a number or decimal string instead.
uintbigintUse integerMode to return a number or decimal string instead.
floatnumberIncludes NaN and infinities; JSON serialization turns them into null.
binarystringHexadecimal representation.
stringstring
booleanboolean
timestampDateJavaScript Date preserves milliseconds, not nanoseconds.
intervalstringFixed-duration ISO 8601 representation.
arraystringJSON text; use JSON.parse() when an array is needed.
objectstringJSON text; use JSON.parse() when an object is needed.
anystringRaw textual value; the SDK does not infer its dynamic JavaScript type.
nullnullA NULL cell is null regardless of the field's declared type.

Integers default to bigint to preserve precision. When returning rows as JSON, choose strings:

const jsonSafeRows = result.toObjects({ integerMode: "string" });
console.log(JSON.stringify(jsonSafeRows));

Use integerMode: "number" only within JavaScript's safe integer range. rawRows() retains original result text, including timestamp precision that Date cannot preserve.

Arrays and objects are JSON text; JSON.parse() does not apply SDK conversions recursively and can lose precision for large nested integers. An any value remains raw text and is not necessarily JSON. See the data types reference for ScopeDB type semantics.

Manage table definitions

Read the table's complete definition and ETag:

const resource = table.catalog();
const snapshot = await resource.get();
console.log(snapshot.definition.columns, snapshot.etag);

To change its comment, preserve the definition and preview the replacement:

const definition = { ...snapshot.definition, comment: "Checkout events" };
const preview = await resource.apply(definition, {
  dryRun: true,
  ifMatch: snapshot.etag,
});
console.log(preview.before, preview.after, preview.destructive);

Review before, after, and destructive. Apply the same definition with the preview's ETag:

const applied = await resource.apply(definition, { ifMatch: preview.etag });
console.log(applied.after?.comment);

apply() replaces the complete definition; omitted fields are cleared. Definitions preserve extension fields. An ETag conflict requires a fresh read and preview. Keep its quotes unchanged. Potentially destructive changes also require allowDestructive: true; this comment edit does not. See the Catalog contract for resource selection and interrupted writes.

Configure queries and writes

Query execution

Use a statement builder to set execution limits or retain a handle for progress and cancellation:

const handle = await client.statement("SELECT 1 AS ready")
  .withMaxElapsedSeconds(60)
  .submit();
console.log(handle.snapshot()?.status);
const queryResult = await handle.wait();
console.log(queryResult.toObjects());

snapshot() reads cached state; refresh() fetches an active statement's state. client.statementHandle(id) resumes tracking a known ID. withMaxElapsedSeconds() includes queueing; withMaxEstimatedSeconds() separately limits estimated duration.

An AbortSignal stops local requests and polling. To request server-side cancellation, call handle.cancel() and inspect the returned snapshot: execution may have finished already or may still be active. Confirmed cancellation makes wait() throw StatementCancelled.

Batching and delivery

Use flush() to wait for buffered writes while keeping a stream open. Streams target 4 MiB batches and allow four concurrent requests by default. Each SDK request is capped at 8 MiB and 200,000 rows.

For a long-running producer that must continue after a failed batch:

const telemetry = table.appendStream({ failurePolicy: "continue" })
  .maxConcurrentBatches(1)
  .circuitBreaker({ failureThreshold: 5, cooldownMs: 30_000 })
  .build();
console.log("Queued:", telemetry.trySend(row));

// Stop producers before shutting down the stream.
const report = await telemetry.shutdown();
console.log(report.outcome, report.committedRows, report.failedRows, report.unknownRows);

trySend() returns false when a row cannot enter the local buffer. Unlike the default stop policy (where flush() and shutdown() return AppendRowsResult | null), continue mode returns an AppendDeliveryReport with outcome, committedRows, failedRows, and unknownRows without retaining failed payloads for replay; use durable storage when loss is unacceptable. Inspect failedRows and unknownRows as well as committed rows. Barrier reports cover activity since the previous returned report; stats() counters cover the stream's lifetime.

Streams retry confirmed temporary rejections by default. retryUnknown(true) explicitly allows replay of uncertain writes and can duplicate rows. maxRetries(0) disables SDK retries; client timeouts and cancellations are never replayed. Do not add another retry loop around a retrying stream.

Row encoding

Streams use JSON.stringify: bigint throws, NaN and infinities become null, and undefined object properties are omitted. Validate numbers with Number.isFinite and integers with Number.isSafeInteger. Date.toISOString() retains only milliseconds.

For existing NDJSON, use await table.append(ndjson). Direct append makes one attempt. Preserve validated NDJSON for exact large integers; quoted decimal strings are not integer input. See the shared JSON input types for accepted field values and schema rules.

Timeouts

Query waiting has no extra SDK deadline unless you supply a signal. Append attempts and Catalog mutations default to 30 seconds, including response-body reads. Configure ClientOptions.writeTimeoutMs, a direct write's timeoutMs, or a stream's attemptTimeoutMs(): omit to inherit, use positive integer milliseconds to override, or null to disable. Zero is invalid.

Streams also have a five-minute per-batch delivery budget, set with maxElapsedTimeMs(), covering attempts and retry delays. Caller cancellation still applies when an SDK deadline is disabled.

Handle errors

Catch ScopeDBError to inspect stable codes and diagnostics:

import { ScopeDBError } from "scopedb";

try {
  await client.query("SELECT 1 AS ready");
} catch (error) {
  if (error instanceof ScopeDBError) {
    console.error(error.code, error.httpStatus, error.requestId);
    console.error(error.statementDetails ?? error.writeDetails);
  }
  throw error;
}

For an Append failure, inspect AppendRowsError.outcome and rowErrors. A temporary classification alone does not make replay safe. A timeout or cancellation after sending may leave the write committed; reconcile unknown outcomes before replaying. Catalog writes never retry automatically; catalog_commit_outcome_unknown requires a read before another mutation.

When retrying direct append() calls, require outcome === "rejected", isTemporary() === true, serverRetryable !== false, and no writeDetails. Honor retryAfterMs and bound attempts and total time. Streams already implement retries; do not wrap them in this loop.

A lost statement submission can report statement_submission_outcome_unknown. Use withStatementId(id) and client.statementHandle(id) to correlate execution; do not blindly resubmit a mutating statement. Follow the HTTP error and retry contract for operation-specific decisions.