# JavaScript SDK

[Read this page in HTML](https://www.scopedb.io/docs/guides/sdks/nodejs)

Query ScopeDB and write events from server-side JavaScript or TypeScript. This guide uses [scopedb 0.3.0](https://www.npmjs.com/package/scopedb/v/0.3.0).

> **Note: Upgrading from JavaScript SDK 0.2?**
>
> Review the [0.3.0 release notes and upgrade steps](https://www.scopedb.io/docs/release-notes/2026-10-23-javascript-sdk-0-3-0.md#upgrade-from-02) before updating.

## Install and configure

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

```bash
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](https://www.scopedb.io/docs/guides/connect-to-scopedb.md) to obtain an endpoint and API key, then set them in the shell where you run the examples:

```bash
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.

```js
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:

```text
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](https://www.scopedb.io/docs/guides/quickstart.md#1-create-an-event-table). This statement leaves an existing table unchanged; if you already have `events`, check that its columns match before continuing.

```js
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:

```js
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:

```js
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 type | JavaScript value | Notes                                                                   |
| ------------------- | ---------------- | ----------------------------------------------------------------------- |
| `int`               | `bigint`         | Use `integerMode` to return a `number` or decimal `string` instead.     |
| `uint`              | `bigint`         | Use `integerMode` to return a `number` or decimal `string` instead.     |
| `float`             | `number`         | Includes `NaN` and infinities; JSON serialization turns them into null. |
| `binary`            | `string`         | Hexadecimal representation.                                             |
| `string`            | `string`         |                                                                         |
| `boolean`           | `boolean`        |                                                                         |
| `timestamp`         | `Date`           | JavaScript `Date` preserves milliseconds, not nanoseconds.              |
| `interval`          | `string`         | Fixed-duration ISO 8601 representation.                                 |
| `array`             | `string`         | JSON text; use `JSON.parse()` when an array is needed.                  |
| `object`            | `string`         | JSON text; use `JSON.parse()` when an object is needed.                 |
| `any`               | `string`         | Raw textual value; the SDK does not infer its dynamic JavaScript type.  |
| `null`              | `null`           | A 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:

```js
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](https://www.scopedb.io/docs/reference/data-types.md) for ScopeDB type semantics.

## Manage table definitions

Read the table's complete definition and ETag:

```js
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:

```js
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:

```js
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](https://www.scopedb.io/docs/api-reference/catalog.md) 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:

```js
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:

```js
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](https://www.scopedb.io/docs/api-reference/append.md#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:

```js
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](https://www.scopedb.io/docs/api-reference.md#errors-and-retries) for operation-specific decisions.

## Related contents

* [SDKs](https://www.scopedb.io/docs/guides/sdks.md)

* [Connect a client](https://www.scopedb.io/docs/guides/connect-to-scopedb.md)

* [API reference](https://www.scopedb.io/docs/api-reference.md)

* [JavaScript SDK repository](https://github.com/scopedb/scopedb-js)
