TypeScript client for the Relata DB data engine — ontology-driven,
enterprise-grade workloads. 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).
Compatible with Node.js 18+, Deno, Bun, and browser environments.
- Repo: github.com/relatadb/sdk-typescript
- Package:
@zysec-ai/relata-sdk - Docs: relatadb.dev · Issues
- Runtime: Node.js 18+, Deno, Bun, browsers (anywhere with native
fetch) - Runtime deps: zero — uses native
fetch, nativecrypto.randomUUID(), nativeAbortController - By: ZySec AI — Frontier Sovereign Intelligence · hello@zysec.ai
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.
npm install @zysec-ai/relata-sdk
# or
yarn add @zysec-ai/relata-sdk
# or
bun add @zysec-ai/relata-sdkimport { createClient } from "@zysec-ai/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-Relata-Tenant-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>();RelataDB auto-detects Cypher: any query starting with MATCH is translated to SQL
before execution. The SDK is language-agnostic — send the string as you would SQL:
const result = await relata.query("MATCH (n:Person {id: 'p1'}) RETURN *");
// → SELECT * FROM Person WHERE id = 'p1'Supported: MATCH / OPTIONAL MATCH, WHERE, RETURN, UNION / UNION ALL
(#378), and CALL traverse.* / CALL gds.* procedures (#377). CREATE / MERGE
writes route through the governed write door. See the
SQL reference.
Memory is a Mem0-style surface over the governed /memory/* verbs — purpose + ACL
stay on by default.
import { Memory } from "@zysec-ai/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)Factory function. Returns a RelataClient.
const relata = createClient("http://localhost:9090", {
bearerToken: "...",
defaultPurpose: "analytics",
timeoutMs: 30_000,
tenant: "org-acme", // X-Relata-Tenant-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
});| Method | Returns | Description |
|---|---|---|
.query<T>(sql, options?) |
Promise<QueryResult<T>> |
Execute raw SQL. Wire shape normalised (see below). |
.select(type) |
QueryBuilder |
Begin 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 (returns taskId for polling) |
.ingestDocumentStatus(taskId) |
Promise<IngestDocumentTaskStatus> |
GET /ingest/document/:task_id — poll an async ingest to completion (#1001) |
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": "..." }.
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".
const relata = createClient(url, {
bearerToken: token,
tenant: "org-acme", // X-Relata-Tenant-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.
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,
});The SDK never writes to console on its own — pass an explicit logger
to get visibility into retry attempts, future deprecation warnings, etc. Two
ready-to-use implementations ship in the box:
import { createClient, ConsoleLogger, NoOpLogger, type Logger } from "@zysec-ai/relata-sdk";
// 1. Use the bundled console logger (CLIs / scripts).
const relata = createClient(url, {
logger: new ConsoleLogger("my-app"), // → "relata:my-app WARN retrying ..."
maxRetries: 3,
});
// 2. Stay silent (default — equivalent to omitting `logger`).
const quiet = createClient(url, { logger: new NoOpLogger() });
// 3. Wire your own observability backend.
const custom: Logger = {
debug: (msg, ctx) => myOtel.span(msg, ctx),
info: (msg, ctx) => myOtel.span(msg, ctx),
warn: (msg, ctx) => myOtel.warn(msg, ctx),
error: (msg, ctx) => myOtel.error(msg, ctx),
};ConsoleLogger writes warn/error to stderr and info/debug to
stdout, so JSON-piping callers never see diagnostics on the data stream.
Each line is shaped <prefix> <LEVEL> <message> followed by an optional
JSON-encoded context object — stable enough for grep but not a substitute
for a real structured logger in production.
The server replies with one of two shapes:
| Wire shape | Meaning |
|---|---|
{"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; // numberMem0-style governed agent memory over /memory/* (ADR-144). Construct with a mandatory
purpose.
import { Memory } from "@zysec-ai/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" });| Method | HTTP | Description |
|---|---|---|
.add(content, opts?) |
POST /memory/remember |
Store a memory; returns its id |
.addBatch(items, opts?) |
POST /memory/remember/batch |
Bulk store; returns index-aligned ids |
.search(query, opts?) |
GET /memory/recall |
Recall ranked by confidence × recency × relevance |
.get(memoryId) |
GET /memory/recognize/:id |
Fetch one memory or null |
.update(memoryId, content) |
POST /memory/consolidate |
Governed supersede; returns new id |
.forget(memoryId) |
DELETE /memory/forget/:id |
Governed retention-policy retract |
.associate(src, tgt, relation, opts?) |
POST /memory/associate |
Link two memories |
.episodes(opts?) |
GET /memory/episodes |
List episodes |
.justify(memoryId) |
GET /memory/justify/:id |
PROV-O assertion chain |
.resolve(memoryId, opts?) |
POST /memory/resolve/:id |
Resolve a contradiction |
.summarise(sourceIds, opts?) |
POST /memory/summarise |
Summary belief from sources |
The MCP envelope ({"content": [{"type":"text","text":"<json>"}]}) is unwrapped
transparently.
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 }>();| Method | Description |
|---|---|
.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> |
Returned by .pathsBetween(a, b, opts?). Same
.purpose / .limit / .withProvenance / .asOf / .execute<T>() surface.
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 "@zysec-ai/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 tools = await mcp.listTools();
const entries = await audit.entries({ purpose: "compliance", limit: 50 });| Module | Class | Surface |
|---|---|---|
governance.ts |
GovernanceClient |
Rules, retention (holds + WORM), breakglass, alerts, DSAR |
mcp.ts |
McpClient |
22+ typed MCP tool wrappers + generic callTool |
a2a.ts |
A2AClient |
A2A tasks + LangGraph checkpoints + agent card |
audit.ts |
AuditClient |
Audit entries (filtered/paginated) + signed receipts + PDF export |
identity.ts |
IdentityClient |
Identity label/uncertainty + lookup tables + ERASE SUBJECT |
objects.ts |
ObjectClient |
Typed upsert + batch via /ingest?object_type= |
ingest.ts |
IngestClient |
Bulk NDJSON + CSV + media status |
vectors.ts |
VectorClient |
KNN + hybrid search + similar-to (SQL-backed) |
s3.ts |
S3Client |
Native-fetch wrapper for the S3 protocol door (no boto3) |
system.ts |
SystemClient |
LLM config + test + jobs status |
streaming.ts |
StreamingClient |
NDJSON row streams + SSE consumers (watch/alerts) + Arrow IPC |
tenants.ts |
TenantAdminClient |
Tenant CRUD + quota + sharing agreements + platform admin |
backup.ts |
BackupClient |
Backup / restore / PITR (admin) |
tokens.ts |
TokenClient |
Dedup / uniqueness tokens (test-and-set) |
log.ts |
LogClient |
Ordered integrity log (append / head / load leaves) |
StreamingClient exposes every streaming surface as an AsyncIterable<T>:
import { StreamingClient } from "@zysec-ai/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.
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 "@zysec-ai/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| Model | Fields |
|---|---|
QueryResult<T> |
rows, queryId, elapsedMs, rowCount, columns |
HealthResponse |
status, profile, nodeId |
StatusResponse |
profile, role, queryQuota |
AuditCountResponse |
entries, chainValid |
ClusterNode |
nodeId, role, url |
IngestDocumentResponse |
reportId, taskId, chunksIngested, warnings, schemaVersion, queueDepth |
VersionInfo |
version, commit, profile, schemaVersion, features |
Stats |
records, states, snapshotRows, logLeaves, tokens, raw |
ReadyReport |
isReady, status, reason, detail |
All errors extend RelataError. Import and catch by type:
import {
PurposeError, AuthError, QuotaError, RateLimitedError, ForbiddenError,
NotFoundError, ConflictError, ValidationError,
BadRequestError, ServerError, NetworkError, TimeoutError,
} from "@zysec-ai/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 class | HTTP | Cause |
|---|---|---|
PurposeError |
400 | Purpose missing or not in registry |
BadRequestError |
400 | SQL syntax error or bad parameter |
AuthError |
401 | Bearer token missing or invalid |
ForbiddenError |
403 | Cedar ACL denies access |
NotFoundError |
404 | Resource does not exist |
ConflictError |
409 | Version or uniqueness conflict |
ValidationError |
422 | Request body failed validation |
RateLimitedError |
429 | Per-principal cost quota or rate limit (extends QuotaError) |
QuotaError |
429 | Back-compat alias for RateLimitedError |
ServerError |
5xx | Server-side error |
NetworkError |
0 | Network failure (DNS, connection refused) |
TimeoutError |
0 | Request exceeded timeoutMs |
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);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 analyticsOptions: --url, --token, --purpose, --timeout, --json.
Exit codes: 0 success · 1 usage / SDK error · 2 audit-chain integrity failure.
Relata extends ANSI SQL with enterprise operators — usable from raw relata.query(sql)
or via the builder:
| Operator | Description |
|---|---|
AS OF 'timestamp' |
Bi-temporal snapshot query |
WITH PROVENANCE |
Attach 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.
| Variable | Default | Description |
|---|---|---|
RELATA_TOKEN |
— | Bearer token for authentication |
RELATA_PURPOSE |
— | Default purpose (CLI helper) |
RELATA_URL |
http://localhost:9090 |
Server URL (examples / CLI) |
PROBE_IMAGE |
— | Path to probe JPEG for face-search example |
See examples/ for runnable workflows:
| File | What it shows |
|---|---|
basic-query.ts |
Health check, raw SQL, typed query, fluent builder |
analytics.ts |
Full analytics workflow (identity, cases, financials, graph) |
face-search.ts |
MATCH_FACE operator with co-occurrence detection |
graph-traversal.ts |
PATHS_BETWEEN, NETWORK_EXPAND, MATCH, Pregel BFS |
audit.ts |
Audit 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
liteis kept as a silent legacy alias forfree(ADR-204).
| Profile | Use case | Start command |
|---|---|---|
free |
Embedded / single-process / dev | RELATA_PROFILE=free relata serve |
server |
Single-node production | RELATA_PROFILE=server relata serve |
cluster |
Multi-node distributed (alpha) | RELATA_PROFILE=cluster relata serve |
The SDK works identically across all three.
src/_ephemeral.ts exports spawnEphemeral() — starts a relata serve process
on a random port, waits for readiness, and returns { port, token, baseUrl, stop }.
import { spawnEphemeral } from './_ephemeral';
describe('integration', () => {
let server: Awaited<ReturnType<typeof spawnEphemeral>>;
beforeAll(async () => { server = await spawnEphemeral(); });
afterAll(() => server.stop());
it('health', async () => {
const r = await fetch(`${server.baseUrl}/health`);
expect(r.ok).toBe(true);
});
});Set RELATA_BIN to the binary path and RELATA_TEST_TOKEN to override the
bearer token (defaults: relata / relata-test).
AGPL-3.0-only — see the root LICENSE file.