TypeScript SDK
Read and write exact Table versions from browser and Node applications.
The TypeScript SDK lets a browser or Node application read and write exact
Table versions as Arrow. The package is @spiraldb/sdk. Table execution uses a hosted service,
a paired local Connect endpoint, or WASM inside a browser worker.
Studio uses the same SDK for hosted and local Table access.
What it covers
- Branch heads, exact Table identities, and Catalog commits.
- Scans with selected columns, computed outputs, filters and limits.
- Positional row reads, top-k results and bounded aggregate results.
- Table writes, blob reads and multipart blob uploads.
- Typed SPQL expressions and data types over Arrow values.
- Local Connect DataFusion SQL queries with incremental Arrow results.
Connect to a stack
connect() selects hosted ScanDAL or WASM execution through scandal.type.
Both modes expose the same Client Table workflows and an asynchronous close() method.
For hosted execution, scandal.type is "remote". Existing hosted configurations without type remain valid.
The SDK authenticates the browser before it returns the client.
Table operations send the shared HttpOnly cookie, and the service obtains storage access for the caller.
Your application does not obtain or forward storage credentials.
export function connectBrowser(token: () => Promise<string>) {
return connect({
apiUrl: "https://api.spiraldb.dev",
token,
scandal: { type: "remote", url: "https://scandal.spiraldb.dev", authUrl: "https://auth.spiraldb.com" },
});
}Register your page's exact origin in the Application's
allowedCorsOrigins. The service checks
those configured origins from the signed token on actual requests. Preflight
OPTIONS permits origins without authentication, with a fixed list of supported
methods and headers. It performs no storage work.
The connection renews its cookie while it is open, including when only native
media is active. Supply a token provider that can obtain a current token, and
handle background renewal failures with onAuthenticationError. Call
close() to stop renewal and clear the shared cookie. disconnect() remains an alias. One top-level site
and Connect origin share an identity; connecting as another identity replaces it.
The connection verifies that the browser returned the cookie and fails explicitly
when browser policy blocks it. It never falls back to credentials in a URL.
For Node, pass transport: "bearer" to the same connect() function. A paired
local host still uses new Client({ scandal: { access, writes } }) or a local
pairing capability. Without scandal, new Client manages resources only.
The low-level explicit storage API remains available for hosts that already
resolve their own access.
Partitioned cookies require Chromium 114+, Firefox 141+, or Safari 26.2+. Safari 18.4 briefly supported them, but 18.5 removed that support. These are platform feature minimums; browser privacy settings and embedded webviews can still prevent cookie return. Cross-site table and blob reads/writes, native media seeking, cookie renewal and logout are tested with Chromium 149.0.7827.55, Firefox 158.0.1 and Safari 27.0.1 on macOS. Embedded webviews need their own compatibility check. Each regional Connect origin needs its own bootstrap. The capability must fit within a 4 KiB cookie, including attributes. HTTPS is required in production; use HTTPS for cross-browser local testing too.
Build a custom hosted reader
Applications that manage their own paging can share the SDK's authenticated
transport through BrowserConnection. Call open() before using it, pass
its fetch to ScandalClient, and call close() when the reader is disposed.
The connection renews authentication and cancels its data requests on close;
it does not resolve storage credentials. Use connect() for the usual Table
and native-media workflows.
Use openScan() with ScanBindings when your reader supplies access for each
split. The returned OpenedScan reads and resumes split streams, and
requireExactFilter() rejects plans requiring client-side residual filtering.
The package exports these helpers and their types from @spiraldb/sdk.
For protocol-level integrations, ScandalContractClient accepts encoded
request bodies and returns contract responses; ScandalClient provides Arrow
adapters. ScandalTypes names the request and response types. The package
also exports exchange() for explicit capability exchange, blobValue() to
convert a stored blob reference without fetching bytes, and uuidv7() for
write-attempt and idempotency identifiers. Import these APIs from the package
root rather than from its internal files.
Show native images and video
Pass a resource-owned blob value to client.blobUrl(). Its offset, length,
version and ETag stay attached to the reference, so a browser range request
addresses the blob's bytes even when it is part of a larger packed object.
Set crossOrigin before src on both image and video elements:
export function showFrame(client: ConnectedClient, frame: types.BlobValue, image: HTMLImageElement) {
image.crossOrigin = "use-credentials";
image.src = client.blobUrl(frame);
}The URL carries the resource reference and contains no access token or storage credential. Native media URLs require cookie transport. For inline bytes, use a browser object URL instead.
Write a Table version and commit it
A Table writes its own new version, and a Catalog commit then publishes one or more of those versions together, so readers of the branch see every change in the commit or none of them. Read the branch head, write the next version from the Table that the head binds, and stage it in a commit onto that head:
import { type Client } from "@spiraldb/sdk";
import { type Table as ArrowTable } from "apache-arrow";
export async function appendFrames(client: Client, projectId: string, rows: ArrowTable) {
const project = client.project(projectId);
const head = await project.branch("main");
const frames = await project.table(head, ["frames"]);
if (!frames) throw new Error("the demo Project has no frames Table");
const next = await frames.append(rows);
return project.commit(head).message("Append frames").updateTable(frames.identity, next).publish();
}append, overwrite, and TableWrite.finish register a version without
changing the Catalog. Commit stages changes without any request:
updateTable moves a Table from the version the head binds to a newer one,
createTable binds a new Table's first version at a root name, and
replaceTable binds a root name to a new Table in place of the one it binds.
On main, createConnection creates a Connection
and stages its name, and dropConnection stages retiring one; both read the
head first. publish sends every staged change as one commit and returns the
branch's new head. A TableWrite uploads large blobs with uploadBlob and
packs many small ones together with packBlobs.
A commit compares only the versions and names it changes, so commits that
change different Tables both succeed. If another commit changed a staged
Table or name first, publish throws PublicationConflictError and publishes
nothing; read the branch again and redo the changes. If publish fails with
the publication_unknown code, the commit may have published: call publish
on the same commit again, which sends the same request, and the Catalog
publishes it at most once. Retries explains
recovery for commits whose outcome is uncertain.
Manage resources
Each service's operations are a property of the client: client.projects,
client.organization, client.catalog, client.tasks, and one for every
other service. They take typed requests and return typed responses, errors,
and page iterators. Studio uses these same operations.
import { type Client } from "@spiraldb/sdk";
export async function listProjects(client: Client) {
for await (const project of client.projects.listProjectsItems({ pageSize: 100 })) {
console.log(project.id, project.description);
}
}A Project adds the workflows that span requests: committing Table
versions, and submitting Tasks and waiting for the versions they publish.
project.submitTask submits any Task definition and returns a TaskHandle;
maintainTable, refreshLocation, and refreshVectorIndex submit the
Tasks they name.
Query through local Connect
Use ConnectClient from @spiraldb/sdk to run a read-only query through the
local SQL API. The host runs
DataFusion on your machine. Supply its
HTTP loopback endpoint and a token callback returning the caller's API access
token. The callback receives minValidityMs: refresh a cached token when its
remaining lifetime is shorter, and reject if a suitable token cannot be obtained.
ConnectClient requests 15 minutes by default, covering the default 840-second
host timeout with a one-minute margin. For a longer host timeout, set
minTokenValidityMs above that timeout. This option requests token lifetime; it
does not change the host's execution limit. Studio's web host refreshes its
session for this requirement before starting local SQL.
SQL uses that bearer for resource resolution and needs no pairing secret. Browser requests must come from an origin registered by the token's client.
import { ConnectClient } from "@spiraldb/sdk";
export async function queryProject(
endpoint: string | URL,
token: (minValidityMs: number) => Promise<string>,
request: { defaultProjectId?: string; defaultRef?: string; sql: string },
signal?: AbortSignal,
) {
const connect = new ConnectClient({ endpoint, token });
const result = await connect.querySql(request, signal);
try {
for await (const batch of result.batches) {
console.log(batch.numRows);
}
return result.schema;
} finally {
await result.cancel();
}
}To include legacy keyed Tables, set legacyTables to a mapping of aliases to
Table IDs and supply defaultProjectId. The host checks each Table's Project
and read access. Aliases are available in the legacy schema. Each distinct
Table is resolved once per query; snapshots across Tables are not atomic.
Connect renews signed storage access
during long scans as the original caller, keeping the selected snapshot. A failed
renewal stops the query. Provider sessions that cannot renew still shorten the
query timeout to their expiry. Queries stop at the host timeout or the original
API token's expiry, whichever comes first.
SQL keeps the host's managed-storage policy, including its local-file restriction.
For legacy Tables, equality on string keys and comparisons on integer keys or
timestamps can narrow storage reads. Combine leading key equalities with a
range on the next key for the smallest scan. You can use ordinary SQL
TIMESTAMP literals with timezone-free columns; comparisons that still require
a precision conversion after SQL optimization remain in DataFusion. Connect
rechecks pushed conditions in DataFusion; other expressions, including OR,
remain there. A SQL limit is applied after the remaining filters, and leading-key
DISTINCT discovery preserves deletion and reinsertion behavior. Filters on
those selected keys run after native discovery, which can avoid reading row data.
For a supported leading-key DISTINCT, batches become available as discovery
finds keys, before the scan finishes. Operations such as sorting or aggregation
can still delay output. Treat the discovered keys as incomplete until the
batches iterator finishes successfully.
querySql returns the Arrow schema and an asynchronous batches iterator.
It streams the complete result; put a SQL limit in the statement when your
application needs a bounded window. Project/ref defaults are optional, and
each resource may supply its own ref. Typed parameters are a base64-encoded
one-row Arrow IPC stream; column positions bind $1 through $N.
Completing iteration verifies the Arrow end marker. Missing markers or body
failures reject completion, even after batches have been yielded. Breaking
iteration, calling cancel(), or aborting its AbortSignal releases the
response. A ConnectError exposes a code and an optional HTTP status.
Queries are never retried or resumed automatically and never fall back to
hosted execution.
Execute file scans in a browser worker
BrowserScandal from @spiraldb/sdk/browser executes Vortex file scans inside
an application worker. Vortex is the columnar file format for these scans.
The host supplies immutable object identities, sizes, and exact byte ranges.
The browser receives Arrow batches without a local Connect process.
OpfsStore provides a persistent cache and temporary files through the
Origin Private File System, or OPFS. The caller supplies one scope per account and Project.
The scope excludes credentials. A Web Lock permits one owner for that scope.
The host obtains storage access before a scan and supplies the range source.
Signed URLs and credentials do not form cache identities.
The default cache budget is zero. Persistent reads require an explicit positive cache budget. The default scratch budget is 512 MiB. Scratch reservations include buffered bytes, and deletion releases those reservations. The store permits at most 128 scratch files and four concurrent source reads. Additional reads wait for admission. Network requests do not block scratch file creation. Cache removal waits for preceding reads. Later reads wait for cache removal.
store.metrics() returns counters since that store opened.
The snapshot reports these measurements:
- Cache hits and misses, including misses whose source reads fail.
- Complete source reads and their byte counts.
- Peaks for cache and scratch reservations.
- The peak count of pending operations.
The cache peak starts after the store evicts excess data when it opens. Source counters exclude failed reads and HTTP overhead. The metrics exclude JavaScript heap allocations.
SpillSort merges temporary runs through scratch files with an estimated
8 MiB memory budget. Its caller must close the sort after an input failure.
Spill, merge, and output periodically yield so that an abort can take effect between records.
Each record encoding and comparison executes synchronously.
This helper does not provide spill support for every ScanDAL operation.
The scanner permits one active scan by default and accepts a limit of up to eight.
It requests one batch at a time and releases the scan on early return or cancellation.
The memoryBytes option checks decoded and encoded output after materialization.
It does not impose a hard heap limit or guarantee a fixed peak memory use.
The generated module is @spiraldb/sdk/wasm.
Its binary subpath is @spiraldb/sdk/wasm/scandal_bg.wasm.
The application serves that binary and initializes the module inside its worker.
The SDK build command npm run build:wasm --workspace @spiraldb/sdk
produces both artifacts from the repository toolchain.
The generated module also exposes openTable for an exact Table version.
Its handle supports scan plans, split reads, positional reads, top-k, and grouped aggregate partials.
The application supplies scoped object callbacks and obtains storage access before execution.
listFunctions describes the installed browser functions.
WasmStorage also prepares Table writes through scoped object callbacks.
It supports row changes, column attachments, change plans, blob streams, and resumable uploads.
The host registers and publishes the prepared version through the platform services.
WasmEvaluator evaluates named expressions over Arrow input and explicitly bound Table readers.
These operations share the browser function catalog.
These browser exports are experimental source interfaces.
They require a compatible WASM module and a dedicated worker.
OpfsStore also requires OPFS and Web Locks.
The raw handles expose the execution contract directly.
Execute Table workflows in a browser worker
BrowserScandalClient connects Client Table workflows to the browser runtime.
Scans reuse the SDK procedures for split retries and result merges.
browserTables resolves versions and scoped storage credentials through the hosted Table service.
It also allocates Tables and registers prepared versions.
These calls do not require an execution capability exchange.
SignedObjectTransport sends storage credentials only to its configured SPFS endpoint.
It verifies that each service binding names that endpoint before execution.
Object requests use the signed URLs and headers that SPFS returns.
The transport reads directly from object storage by default, without OPFS.
Explicit browser hosts can supply an OpfsStore with a positive cache budget for persistent reads.
Cached ranges use the account and Project scope of that store.
Cache identities exclude credentials and signed URLs.
The SPFS CORS policy must permit PUT with Authorization and Content-Type. Object storage must permit the signed request methods and headers. Cross-origin reads require CORS access to Content-Range and ETag. Versioned S3 objects also require access to x-amz-version-id.
For browser execution, scandal.type is "wasm".
The SDK initializes the packaged module and constructs the backend and storage transport.
The factory rejects calls outside a dedicated browser worker.
The generated module resolves its matching binary. The optional wasmUrl field overrides that location.
The host supplies a module loader through scandal.wasm, for example () => import("@spiraldb/sdk/wasm").
Only the WASM application bundles that import. Hosted applications do not require generated WASM assets.
Connections with the same module share initialization inside their worker.
export function configureWorker(token: () => Promise<string>, wasm: () => Promise<WasmModule>) {
return connect({
apiUrl: "https://api.spiraldb.dev",
token,
scandal: {
type: "wasm",
wasm,
spfsUrl: "https://spfs.spiraldb.dev",
},
});
}OPFS remains optional. The scandal.storage configuration creates a store with an explicit account and Project scope.
A positive cacheBytes budget enables persistent reads. Without that budget, the store provides scratch storage only.
The client exposes this store as client.browserStorage. The separate client.storage property remains the hosted storage service.
export function configureWorkerStorage(token: () => Promise<string>, wasm: () => Promise<WasmModule>, scope: string) {
return connect({
apiUrl: "https://api.spiraldb.dev",
token,
scandal: {
type: "wasm",
wasm,
spfsUrl: "https://spfs.spiraldb.dev",
storage: {
type: "opfs",
scope,
cacheBytes: 512 * 1024 * 1024,
scratchBytes: 512 * 1024 * 1024,
},
},
});
}client.close() cancels queued and active execution, drains object callbacks, and releases the OPFS lock.
The default preserves cached bytes across connections. close({ clearCache: true }) removes cached bytes after execution drains and before lock release.
A failed deletion rejects close and retains the lock for a retry. Outstanding scratch files also prevent lock release.
A later close attempt can succeed after those files are removed.
Close each SpillSort helper before the client.
For session cleanup, request cache removal on the initial close call:
export async function closeBrowserSession(client: WasmConnectedClient) {
await client.close({ clearCache: true });
}A cache removal request after a completed cache-preserving close fails explicitly. The caller must request removal before lock release.
Browser security and resource limits
The module loader is trusted application code, with the same JavaScript access as the rest of the application. The browser holds scoped storage capabilities in memory. An application script with XSS access can use those capabilities within their scope and lifetime. The SDK refuses vended provider credentials and verifies the configured SPFS endpoint before it sends a storage capability. Object requests omit browser cookies and reject redirects. This mode does not bypass object storage permissions, CORS rules, or IP restrictions.
OPFS stores application bytes without SDK encryption. Account and Project scopes separate cache names. They do not provide an authorization boundary within an origin.
close() does not perform application sign-out or revoke issued credentials. Token expiry and revocation remain service responsibilities.
For sensitive data, use close({ clearCache: true }) during session cleanup.
Clear the application's token provider during sign-out.
Use a new connection and cache scope after an account change.
The lower-level BrowserScandalClient, SignedObjectTransport, and browserTables exports remain available for custom hosts.
The browser backend provides one execution slot by default, with a maximum of eight slots. Its queue permits at most 64 requests before a budget error.
A stream releases its execution slot between batches or blob pulls. A consumer can call another backend method before it requests the next batch. At most eight operations retain runtime handles, including suspended streams.
Admission precedes Arrow encoding and WASM copies. Each split supplies one batch at a time. Cancellation releases runtime handles and drains object callbacks before it returns the current execution slot.
Multipart uploads require S3-compatible storage behind SPFS. An abort waits for active part requests to settle.
Each upload part permits 30 seconds plus one second per 64 KiB. Caller cancellation interrupts an active part immediately. Upload initiation and cleanup use separate bounded deadlines. The transport also aborts an upload whose identifier arrives after cancellation.
It rejects vended credentials and unregistered browser functions explicitly. Legacy keyed Tables and SQL remain outside this browser backend. Vector Index searches require a native or hosted backend.
memoryBytes sets the tracked runtime budget, with a default of 64 MiB.
Arrow encoding and JavaScript result merges also allocate memory.
The budget does not impose a hard heap limit. Large or malformed inputs can still exhaust browser memory.
A host can terminate its worker after a deadline. Worker termination stops execution but does not remove persistent cache bytes.
Generic operator spill remains unavailable.
The separate SpillSort helper uses OPFS scratch files.
If the host opens an OPFS store directly, close it after all queries and writes complete.
Availability
Install @spiraldb/sdk from npm.
Studio uses the same ConnectClient for local SQL.