Skip to Content
SDKsTypeScript

TypeScript SDK

⏱ Running in 5 minutes — new here? Jump to the quickstart for install → first query → first agent memory, or copy the Quick start block below. npm i @relata/sdk and you’re ready.

The official TypeScript SDK for RelataDB. Zero runtime dependencies (native fetch only), typed responses, fluent query builder, a Mem0-style governed memory client, and 12+ typed v1.1 clients that mirror the server’s REST surface — all with full transport hardening (RFC 7807 problem+json, X-Request-ID, retry, multi-tenant headers).

  • Source: sdks/typescript/ (sdks/typescript/src/)
  • Package: @relata/sdk
  • Runtime: Node.js 18+, Deno, Bun, browsers (anywhere with native fetch)
  • Runtime deps: zero — uses native fetch, native crypto.randomUUID(), native AbortController
  • Parity: tracks the Python reference SDK (sdks/python/relata/) method-for-method

TypeScript is async-native, so every SDK method returns a Promise. There are no separate Async* classes — that is the idiomatic parity with the Python SDK’s sync/async split.

Install

npm install @relata/sdk # or yarn add @relata/sdk # or bun add @relata/sdk

Quick start

import { createClient } from "@relata/sdk"; const relata = createClient("http://localhost:9090", { bearerToken: process.env.RELATA_TOKEN, // required when server sets RELATA_BEARER_TOKEN defaultPurpose: "analytics", // required — every query must declare a purpose tenant: "org-acme", // X-Organization-Id (multi-tenant) timeoutMs: 15_000, maxRetries: 3, // retry on 502/503/504 + network errors }); // Raw SQL query const result = await relata.query("SELECT * FROM Person LIMIT 10"); console.log(result.rows); // Typed query interface Person { id: string; name: string; dob?: string } const persons = await relata.query<Person>( "SELECT id, name, dob FROM Person LIMIT 5", ); persons.rows[0]?.name; // typed: string | undefined // Fluent builder const recent = await relata .select("Person") .where("name LIKE 'Ahmed%'") .asOf("2025-01-01T00:00:00Z") .withProvenance() .orderBy("name") .limit(20) .execute<Person>();

Agent memory in three lines

Memory is a Mem0-style surface over the governed /memory/* verbs — purpose + ACL stay on by default.

import { Memory } from "@relata/sdk"; const m = new Memory("http://localhost:9090", { purpose: "agent-notes", bearerToken: process.env.RELATA_TOKEN, }); const memId = await m.add("Alice prefers dark mode"); // store const hits = await m.search("ui preferences", { topK: 5 }); // recall (confidence × recency × relevance) await m.forget(memId); // governed retract, not a hard delete await m.close(); // no-op on native fetch (API symmetry)

createClient(baseUrl, options?)

Factory function. Returns a RelataClient.

const relata = createClient("http://localhost:9090", { bearerToken: "...", defaultPurpose: "analytics", timeoutMs: 30_000, tenant: "org-acme", // X-Organization-Id actingAs: "user-bob", // X-Acting-As (delegation) delegatedBy: "user-alice", // X-Delegated-By maxRetries: 3, // retry 502/503/504 + network errors retryBackoffMs: 500, // exponential base (default 500ms) headers: { // arbitrary caller headers, win over SDK defaults "X-Verified-Principal": "proxy-client", }, fetch: customFetch, // override for testing / observability });

RelataClient — the main client

MethodReturnsDescription
.query<T>(sql, options?)Promise<QueryResult<T>>Execute raw SQL. Wire shape normalised (see below).
.select(type)QueryBuilderBegin a fluent query
.health()Promise<HealthResponse>GET /health — liveness
.status()Promise<StatusResponse>GET /status — profile, role, quota
.stats()Promise<Stats>GET /debug/stats — engine counts
.version()Promise<VersionInfo>GET /version — build info
.ready()Promise<ReadyReport>GET /health/ready — 9-condition readiness
.auditCount()Promise<AuditCountResponse>GET /audit/count — entry count + chainValid
.clusterNodes()Promise<ClusterNode[]>GET /cluster/nodes — list cluster members
.ingestDocument(chunksJsonl, manifestJson)Promise<IngestDocumentResponse>POST /ingest/document — datagrep-extractor envelope

Every method is async (TS is async-native). The X-Request-ID header is auto-generated per request via crypto.randomUUID() — pin your own by setting headers: { "X-Request-ID": "..." }.

Purpose (mandatory)

Every query must declare a purpose registered in the tenant’s PurposeRegistry (SPECS §5.22.4). The server rejects purposeless queries with HTTP 400 (PurposeError).

// Client-level default const relata = createClient(url, { defaultPurpose: "analytics" }); // Per-query override await relata.query("SELECT ...", { purpose: "audit" }); // Via builder relata.select("Person").purpose("audit").execute();

Common values: "analytics", "operations", "security_incident", "compliance_review", "audit".

Multi-tenant + delegation headers

const relata = createClient(url, { bearerToken: token, tenant: "org-acme", // X-Organization-Id actingAs: "user-bob", // X-Acting-As delegatedBy: "user-alice", // X-Delegated-By });

Without a token the SDK runs as the built-in api-user principal — fine for local dev, not for production multi-tenant.

Retry behaviour

maxRetries (default 0 = off) retries on HTTP {502, 503, 504} and on raw network errors (DNS, connection refused). Backoff is exponential: retryBackoffMs * 2^attempt. Timeouts are never retried (operations may have side-effects).

const relata = createClient(url, { defaultPurpose: "analytics", maxRetries: 3, retryBackoffMs: 500, });

QueryResult wire-shape normalisation

The server replies with one of two shapes:

Wire shapeMeaning
{"data": [...rows...], "query_id": "...", "elapsed_ms": N}Rich (row data in data)
{"rows": <int count>, "query_id": "...", "elapsed_ms": N}Legacy (row count in rows)

The SDK normalises both so callers always see result.rows as an array, matching the Python _normalise_wire_shape validator:

const result = await relata.query("SELECT * FROM Person LIMIT 5"); result.rows; // Record<string, unknown>[] — always an array result.rowCount; // number — always rows.length result.columns; // string[] — column names when the server sends them result.queryId; // string result.elapsedMs; // number

Memory — agent memory

Mem0-style governed agent memory over /memory/* (ADR-144). Construct with a mandatory purpose.

import { Memory } from "@relata/sdk"; const m = new Memory("http://localhost:9090", { purpose: "agent-notes", bearerToken: process.env.RELATA_TOKEN, sessionId: "sess-1", // optional default session }); const id1 = await m.add("Alice prefers dark mode"); const id2 = await m.add("Bob likes light mode", { memoryClass: "episodic", confidence: 0.8 }); const ids = await m.addBatch(["first", "second", { content: "third", confidence: 0.5 }]); const hits = await m.search("ui preferences", { topK: 5, asOf: "2025-01-01" }); const mem = await m.get(id1); // Record<string, unknown> | null const newId = await m.update(id1, "Alice prefers dark mode (revised)"); const decision = await m.forget(id1); // governed retention-policy retract // The five cognitive verbs (#77): await m.associate(id1, id2, "contradicts", { confidence: 0.9 }); const eps = await m.episodes({ sessionId: "sess-1" }); const chain = await m.justify(id1); const resolved = await m.resolve(id1, { policy: "highest_confidence" }); const summary = await m.summarise([id1, id2], { summaryContent: "both prefer different modes" });
MethodHTTPDescription
.add(content, opts?)POST /memory/rememberStore a memory; returns its id
.addBatch(items, opts?)POST /memory/remember/batchBulk store; returns index-aligned ids
.search(query, opts?)GET /memory/recallRecall ranked by confidence × recency × relevance
.get(memoryId)GET /memory/recognize/:idFetch one memory or null
.update(memoryId, content)POST /memory/consolidateGoverned supersede; returns new id
.forget(memoryId)DELETE /memory/forget/:idGoverned retention-policy retract
.associate(src, tgt, relation, opts?)POST /memory/associateLink two memories
.episodes(opts?)GET /memory/episodesList episodes
.justify(memoryId)GET /memory/justify/:idPROV-O assertion chain
.resolve(memoryId, opts?)POST /memory/resolve/:idResolve a contradiction
.summarise(sourceIds, opts?)POST /memory/summariseSummary belief from sources

The MCP envelope ({"content": [{"type":"text","text":"<json>"}]}) is unwrapped transparently.

QueryBuilder — fluent SQL

Fluent builder returned by relata.select(type).

const result = await relata .select("Person") .purpose("analytics") .where("name LIKE 'Ahmed%'") .asOf("2025-01-01T00:00:00Z") .withProvenance() .orderBy("name") .limit(20) .execute<{ id: string; name: string }>();
MethodDescription
.columns(...cols)Columns to select (* by default)
.purpose(p)Override purpose for this query
.where(condition)Append AND condition
.asOf(timestamp)Bi-temporal AS OF clause
.withProvenance()Attach PROV-O metadata
.orderBy(col, dir?)Add ORDER BY clause (ASC or DESC)
.limit(n)Set LIMIT (positive integer)
.offset(n)Set OFFSET (non-negative integer)
.pathsBetween(a, b, opts?)Switch to PATHS_BETWEEN sub-builder
.toSQL()Return SQL string without executing
.execute<T>()Execute and return QueryResult<T>

PathsQueryBuilder

Returned by .pathsBetween(a, b, opts?). Same .purpose / .limit / .withProvenance / .asOf / .execute<T>() surface.

Typed v1.1 clients — fromClient(client)

Each typed client inherits the parent client’s auth, tenant, and header context, so governance stays consistent across the surface. TypeScript is async-native, so every method returns a Promise (no Async* classes).

import { createClient, GovernanceClient, McpClient, AuditClient } from "@relata/sdk"; const relata = createClient(url, { bearerToken: token, defaultPurpose: "compliance", tenant: "acme" }); const gov = GovernanceClient.fromClient(relata); const mcp = McpClient.fromClient(relata); const audit = AuditClient.fromClient(relata); const rule = await gov.createRule({ name: "big-xfer", object_type: "Transaction", condition: "amount_usd > 1000000", action: "alert" }); const sigma = await gov.importSigma(open("rules.yml", "utf8")); const holds = await gov.listLegalHolds(); const tools = await mcp.listTools(); const entries = await audit.entries({ purpose: "compliance", limit: 50 });
ModuleClassSurface
governance.tsGovernanceClientRules, retention (holds + WORM), breakglass, alerts, DSAR
mcp.tsMcpClient22+ typed MCP tool wrappers + generic callTool
a2a.tsA2AClientA2A tasks + LangGraph checkpoints + agent card
audit.tsAuditClientAudit entries (filtered/paginated) + signed receipts + PDF export
identity.tsIdentityClientIdentity label/uncertainty + lookup tables + ERASE SUBJECT
objects.tsObjectClientTyped upsert + batch via /ingest?object_type=
ingest.tsIngestClientBulk NDJSON + CSV + media status
vectors.tsVectorClientKNN + hybrid search + similar-to (SQL-backed)
s3.tsS3ClientNative-fetch wrapper for the S3 protocol door (no boto3)
system.tsSystemClientLLM config + test + jobs status
streaming.tsStreamingClientNDJSON row streams + SSE consumers (watch/alerts) + Arrow IPC
tenants.tsTenantAdminClientTenant CRUD + quota + sharing agreements + platform admin
backup.tsBackupClientBackup / restore / PITR (admin)
tokens.tsTokenClientDedup / uniqueness tokens (test-and-set)
log.tsLogClientOrdered integrity log (append / head / load leaves)

Streaming — async iterables

StreamingClient exposes every streaming surface as an AsyncIterable<T>:

import { StreamingClient } from "@relata/sdk"; const streaming = StreamingClient.fromClient(relata); // NDJSON row stream for await (const row of streaming.queryRows("SELECT * FROM Person", { purpose: "analytics" })) { console.log(row); } // SSE watch for await (const ev of streaming.watch("SELECT * FROM Person", "analytics")) { console.log(ev); // { event: "RowsAppended", ... } } // SSE alerts for await (const alert of streaming.alerts()) { console.log(alert); } // Raw Arrow IPC byte stream for await (const chunk of streaming.queryArrowRaw("SELECT * FROM Person", { purpose: "analytics" })) { // chunk: Uint8Array — feed to a columnar reader }

SSE consumers reconnect with exponential backoff until you break out of the for await loop.

S3 door — native fetch only

The Python SDK ships boto3 / aiobotocore / httpx flavours; this TypeScript port ships only the native-fetch equivalent (S3Client.http(method, path, opts)) because boto3 is Python-only and the TS SDK has zero runtime dependencies. Convenience wrappers (listBuckets, createBucket, putObject, getObject, deleteObject) are included.

import { S3Client } from "@relata/sdk"; const s3 = S3Client.fromClient(relata); await s3.createBucket("acme-intel"); await s3.putObject("acme-intel", "report.pdf", pdfBytes, { contentType: "application/pdf" }); const obj = await s3.getObject("acme-intel", "report.pdf"); console.log(obj.body); // Uint8Array

Response models

ModelFields
QueryResult<T>rows, queryId, elapsedMs, rowCount, columns
HealthResponsestatus, profile, nodeId
StatusResponseprofile, role, queryQuota
AuditCountResponseentries, chainValid
ClusterNodenodeId, role, url
IngestDocumentResponsereportId, chunksIngested, warnings, schemaVersion, queueDepth
VersionInfoversion, commit, profile, schemaVersion, features
Statsrecords, states, snapshotRows, logLeaves, tokens, raw
ReadyReportisReady, status, reason, detail

Error handling

All errors extend RelataError. Import and catch by type:

import { PurposeError, AuthError, QuotaError, RateLimitedError, ForbiddenError, NotFoundError, ConflictError, ValidationError, BadRequestError, ServerError, NetworkError, TimeoutError, } from "@relata/sdk"; try { const r = await relata.query("SELECT * FROM Person LIMIT 10"); } catch (err) { if (err instanceof PurposeError) { console.error("Fix:", err.message); } else if (err instanceof RateLimitedError) { console.error(`Rate limited. Retry after: ${err.retryAfterSeconds}s`); } else if (err instanceof NotFoundError) { console.error(`Not found. Code: ${err.code}, requestId: ${err.requestId}`); } else if (err instanceof TimeoutError) { console.error(`Timed out after ${err.timeoutMs}ms`); } else if (err instanceof NetworkError) { console.error("Cannot reach server:", err.cause); } else { throw err; } }

Every error carries the RFC 7807 problem+json fields (code, typeUrl, retryable, requestId) when the server emits them. RateLimitedError extends QuotaError so existing catch (e instanceof QuotaError) callers keep working.

Error classHTTPCause
PurposeError400Purpose missing or not in registry
BadRequestError400SQL syntax error or bad parameter
AuthError401Bearer token missing or invalid
ForbiddenError403Cedar ACL denies access
NotFoundError404Resource does not exist
ConflictError409Version or uniqueness conflict
ValidationError422Request body failed validation
RateLimitedError429Per-principal cost quota or rate limit (extends QuotaError)
QuotaError429Back-compat alias for RateLimitedError
ServerError5xxServer-side error
NetworkError0Network failure (DNS, connection refused)
TimeoutError0Request exceeded timeoutMs

Async

TypeScript is async-native — every method returns a Promise. There are no separate Async* classes. Use async/await directly:

const relata = createClient("http://localhost:9090", { defaultPurpose: "analytics" }); const result = await relata.query("SELECT * FROM Person LIMIT 5"); for (const row of result.rows) console.log(row);

Custom object types

RelataDB is ontology-governed — unknown types are rejected on both ingest and read (fail-closed). Register custom types at runtime via POST /types:

const r = await relata.fetchImpl( `${relata.baseUrl}/types`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ name: "AgentTask", fields: [ { name: "task_id", type: "string" }, { name: "status", type: "string" }, ], }), }, );

For ACL access in strict mode, grant via env var: RELATA_ACL_GRANT=AgentTask:read+write.

CLI helper

npx relata health --url http://localhost:9090 npx relata status npx relata audit npx relata nodes npx relata query "SELECT * FROM Person LIMIT 5" --purpose analytics

Options: --url, --token, --purpose, --timeout, --json.

Extended SQL dialect

Relata extends ANSI SQL with enterprise operators — usable from raw relata.query(sql) or via the builder:

OperatorDescription
AS OF 'timestamp'Bi-temporal snapshot query
WITH PROVENANCEAttach PROV-O provenance to rows
PATHS_BETWEEN(a, b, max_hops => 4)Shortest graph paths
NETWORK_EXPAND(seed_id => ..., hops => 3)Network expansion
MATCH_FACE(image_bytes => ..., threshold => 0.70)Face recognition
LOOKUP_IDENTITY(column, value)IdentityIndex universal lookup
HYBRID_SCORE(...)Combined BM25 + vector similarity
PREGEL_BFS(seed_id => ..., max_rounds => 4)Iterative graph BFS
GENERATE_REPORT(type => ..., period => ...)Signed compliance report
SIMILAR TO <Type> WHERE id = '...'Multi-vector similarity
ERASE SUBJECT '<id>' REASON '<r>' [CERTIFY]GDPR Art. 17 crypto-shred

See the SQL reference for full syntax.

Environment variables

VariableDefaultDescription
RELATA_TOKENBearer token for authentication
RELATA_PURPOSEDefault purpose (CLI helper)
RELATA_URLhttp://localhost:9090Server URL (examples / CLI)
PROBE_IMAGEPath to probe JPEG for face-search example

Examples

See examples/ for runnable workflows:

FileWhat it shows
basic-query.tsHealth check, raw SQL, typed query, fluent builder
analytics.tsFull analytics workflow (identity, cases, financials, graph)
face-search.tsMATCH_FACE operator with co-occurrence detection
graph-traversal.tsPATHS_BETWEEN, NETWORK_EXPAND, MATCH, Pregel BFS
audit.tsAudit chain verification, anomaly detection, compliance report

Run any example:

RELATA_TOKEN=secret node --experimental-strip-types examples/basic-query.ts # or deno run --allow-net examples/basic-query.ts # or bun run examples/basic-query.ts

Deployment profiles

ProfileUse caseStart command
liteEmbedded / single-process / devRELATA_PROFILE=lite relata serve
serverSingle-node productionRELATA_PROFILE=server relata serve
clusterMulti-node distributed (alpha)RELATA_PROFILE=cluster relata serve

The SDK works identically across all three.

License

AGPL-3.0-only — see the root LICENSE file.

Last updated on