Reference

SDK Cheat Sheet

One-page reference for common Onyx SDK calls. Select a language to see the syntax.

Onyx Environment Variables

Set these to control how the SDK scopes requests and where it fetches credentials.

TypeScript
Env VarDescriptionDefault
ONYX_DATABASE_IDDatabase UUID used to scope requests; required.none
ONYX_DATABASE_BASE_URLHTTP base for DB API.https://api.onyx.dev
ONYX_DATABASE_API_KEYAPI key for the database; required.none
ONYX_DATABASE_API_SECRETAPI secret for the database; required.none
ONYX_AI_BASE_URLBase URL for AI/chat endpoints.https://ai.onyx.dev
ONYX_DEFAULT_MODELModel used by db.chat() shorthand.onyx
ONYX_CONFIG_PATHPath to JSON credentials file (Node only; ignored on edge). Falls back to project/home files after env vars.unset
ONYX_DEBUGWhen "true", enables request/response logging and config debug output.false
ONYX_STREAM_DEBUGWhen "true"/"1", logs streaming connection details.false

Initialization Patterns

Initialize the SDK with typed configuration.

TypeScript

Auto resolution

Resolution order: env vars → ONYX_CONFIG_PATH file → ./onyx-database.json (default locations).

import { onyx } from "@onyx.dev/onyx-database";
import { Schema, tables } from "./generated/onyx/types";

const db = onyx.init<Schema>();

Initialize with config object

Pass credentials directly when you can’t rely on env/file resolution.

import { onyx } from "@onyx.dev/onyx-database";
import { Schema, tables } from "./generated/onyx/types";

const db = onyx.init<Schema>({
  apiKey: process.env.ONYX_DATABASE_API_KEY,
  apiSecret: process.env.ONYX_DATABASE_API_SECRET,
});

Optional config fields

AttributeDescriptionDefault
baseUrlREST base URL for database operations.https://api.onyx.dev
databaseIdDatabase ID. Usually inferred from DB-scoped API keys; set for org-wide keys.resolved from API key
aiBaseUrlBase URL for AI endpoints.https://ai.onyx.dev
fetchCustom fetch implementation (useful in non-Node runtimes).global fetch
defaultModelFallback AI model when using shorthand chat calls.onyx
partitionDefault partition for queries, findById, and deletes.none (use entity partition)
requestLoggingEnabledLog HTTP requests and bodies to console.false
responseLoggingEnabledLog HTTP responses and bodies to console.false
ttlMilliseconds to cache resolved credentials.300000 (5 minutes)
retryRetry configuration for idempotent GET requests (honors Retry-After).enabled; 3 retries; backoff 300/600/1200ms
  • Typed vs. Untyped ClientsFor full IntelliSense and type safety, run onyx gen and initialize with onyx.init<Schema>(). Only use the untyped onyx.init() for quick prototyping.
  • Retry LogicIdempotent GET requests automatically retry 3 times with exponential backoff (300ms → 1.2s) and honor Retry-After headers. You can override defaults via the retry config object.
  • Performance & CachingConfig resolves once (Env → File → Defaults) and caches per databaseId-apiKey for 5 minutes. Always reuse the client instance to utilize the cache. Clear manually via onyx.clearCacheConfig().

Core CRUD

Upsert, fetch, update, and delete entities by primary key.

TypeScript
import { onyx } from "@onyx.dev/onyx-database";
import { Schema, tables, Account } from "./generated/onyx/types";

const db = onyx.init<Schema>();

const account: Account = {
  id: "acct_1",
  name: "Checking",
  balance: 1250,
};

await db.save(tables.Account, account);

const fetched = await db.findById(tables.Account, "acct_1");

await db.save(tables.Account, { ...account, balance: 1400 });

await db.delete(tables.Account, "acct_1");

TypeScript return types

onyx.init<Schema>(): IOnyxDatabase<Schema>
db.save(tables.Account, account): Promise<Account>
db.findById(tables.Account, id): Promise<Account | null>
db.save(tables.Account, partialAccount): Promise<Account> // upsert
db.delete(tables.Account, id): Promise<boolean>

Atomic Saving

Persist a parent plus related records in one call—no explicit transactions needed; the graph commits all-or-none.

TypeScript

Cascade save

Tell Onyx which related collection to save with the parent. Format: field:RelatedTable(targetField, sourceField). Assumes db is already initialized and tables is imported.

const accountWithTransactions = {
  id: "acct_1",
  name: "Primary",
  balance: 0,
  transactions: [
    { id: "txn_1", accountId: "acct_1", amount: 250, currency: "USD" },
    { id: "txn_2", accountId: "acct_1", amount: 125, currency: "USD" },
  ],
};

const savedAccount = await db
  .cascade("transactions:Transaction(accountId, id)")
  .save(tables.Account, accountWithTransactions);

Cascade builder

Fluent builder emits the same cascade string; helpful when composing or reusing mappings.

const txRel = db
  .cascadeBuilder()
  .graph("transactions")
  .graphType("Transaction")
  .targetField("accountId")
  .sourceField("id");

const cascadeString = String(txRel);
console.log("cascade string:", cascadeString); // transactions:Transaction(accountId, id)

Cascade string syntax

<field>:<RelatedTable>(<targetField>, <sourceField>) — field: property on the parent holding related rows; RelatedTable: table to upsert; targetField: FK on the related table; sourceField: parent field to copy.

TypeScript return types

onyx.init<Schema>(): IOnyxDatabase<Schema>
db.cascade(...): ICascadeBuilder<Schema>
db.cascade(...).save(tables.Account, entity): Promise<Account>
db.cascadeBuilder(): ICascadeRelationshipBuilder
cascadeBuilder().graph(...).graphType(...).targetField(...).sourceField(...): string
db.from(tables.Account): IQueryBuilder<Account>
resolve("transactions.lineItems"): IQueryBuilder<Account>
firstOrNull(): Promise<Account | null>

Resolvers

Hydrate schema-defined relations inline to avoid extra round trips; resolvers are declared in your generated schema.

TypeScript

Fetch by id with resolver

Load a record and its related resolver in one call.

import { tables } from "./generated/onyx/types";

const tx = await db.findById(tables.Transaction, "txn_123", { resolvers: ["account"] });

Query with resolver

Include related data while filtering the base table.

import { eq } from "@onyx.dev/onyx-database";
import { tables } from "./generated/onyx/types";

const transactionsWithAccounts = await db
  .from(tables.Transaction)
  .where(eq("status", "posted"))
  .resolve("account")
  .list();

Filter by resolver field

Filter using fields exposed by the resolver.

import { eq } from "@onyx.dev/onyx-database";
import { tables } from "./generated/onyx/types";

const primaryAccountTransactions = await db
  .from(tables.Transaction)
  .where(eq("account.name", "Primary"))
  .resolve("account")
  .list();

Resolver names match the resolvers entries in your schema (e.g., a Transaction → account relation). They’re generated into the typed client so you can request related data without manual joins.

TypeScript return types

findById(..., { resolvers }): Promise<Transaction | null>
resolve("account"): IQueryBuilder<Transaction>
where(eq("account.name", ...)): IQueryBuilder<Transaction>
list(): QueryResultsPromise<Transaction>

Bulk Save

Insert many records efficiently by passing an array to save.

TypeScript
import { tables } from "./generated/onyx/types";

const transactions = [
  { id: "txn_batch_1", accountId: "acct_batch", amount: 1.99, currency: "USD" },
  { id: "txn_batch_2", accountId: "acct_batch", amount: 12.5, currency: "USD" },
  { id: "txn_batch_3", accountId: "acct_batch", amount: 45.0, currency: "USD" },
  { id: "txn_batch_4", accountId: "acct_batch", amount: 7.25, currency: "USD" },
];

await db.save(tables.Transaction, transactions);

TypeScript return types

db.save(tables.Transaction, transactions): Promise<Transaction[]>

Query & Filter

Boolean filters, ranges, text contains, ordering, and limits.

TypeScript
import { eq, gt, gte, contains, desc } from "@onyx.dev/onyx-database";
import { tables } from "./generated/onyx/types";

const transactions = await db
  .from(tables.Transaction)
  .where(
    eq("status", "posted")
      .and(gt("amount", 100))
      .and(gte("createdAt", "2025-01-01"))
      .and(contains("merchant", "aws"))
  )
  .orderBy(desc("createdAt"))
  .limit(25)
  .list();

Defaults: if you omit limit() and pageSize, the API uses its server-side default page size (capped at 1000); responses include nextPage when more rows remain.

TypeScript return types

from(tables.Transaction): IQueryBuilder<Transaction>
eq()/gt()/gte()/contains(): ConditionBuilderImpl (IConditionBuilder)
where(...): IQueryBuilder<Transaction>
orderBy(desc("createdAt")): IQueryBuilder<Transaction>
limit(25): IQueryBuilder<Transaction>
list(): QueryResultsPromise<Transaction> // await -> QueryResults<Transaction> & Transaction[]

Query clauses

ClauseSignatureWhat it does
wherewhere(condition)Sets the primary filter.
andand(condition)Adds an additional filter with AND.
oror(condition)Adds an additional filter with OR.
orderByorderBy(asc/desc(...))Sorts results by one or more fields.
groupBygroupBy(...fields)Groups records for aggregations.
distinctdistinct()Returns only unique rows for selected fields.
limitlimit(n)Caps number of records returned.
pageSizepageSize(n)Sets page size for list/page calls and streams.
inPartitioninPartition("partition")Scopes the query to a partition.
nextPagenextPage(token)Continues a paged query using a cursor token.

Filters

HelperSignatureWhat it does
eqeq("field", value)Equals.
neqneq("field", value)Not equals.
gtgt("field", value)Greater than.
gtegte("field", value)Greater than or equal.
ltlt("field", value)Less than.
ltelte("field", value)Less than or equal.
betweenbetween("field", low, high)Inclusive range.
likelike("field", pattern)SQL-like match (%, _).
notLikenotLike("field", pattern)Negated SQL-like match.
matchesmatches("field", regex)Regex match.
notMatchesnotMatches("field", regex)Regex does not match.
containscontains("field", text)Substring contains (case-sensitive).
containsIgnoreCasecontainsIgnoreCase("field", text)Substring contains (case-insensitive).
notContainsnotContains("field", text)Does not contain (case-sensitive).
notContainsIgnoreCasenotContainsIgnoreCase("field", text)Does not contain (case-insensitive).
startsWithstartsWith("field", text)Prefix match.
notStartsWithnotStartsWith("field", text)Not prefix match.
inOpinOp("field", values)Field in list of values.
notInnotIn("field", values)Field not in list of values.
withinwithin("field", subquery)Field in subquery results.
notWithinnotWithin("field", subquery)Field not in subquery results.
isNullisNull("field")Field is null.
notNullnotNull("field")Field is not null.

Select Query

Pick specific columns to return from a table.

TypeScript
import { eq } from "@onyx.dev/onyx-database";
import { tables } from "./generated/onyx/types";

const rows = await db
  .select("id", "accountId", "amount", "createdAt")
  .from(tables.Transaction)
  .list();

TypeScript return types

select("id","accountId",...): IQueryBuilder<Pick<Transaction, "id" | "accountId" | "amount" | "createdAt">>
from(tables.Transaction): IQueryBuilder<...>
where(eq(...)): IQueryBuilder<...>
orderBy(desc("createdAt")): IQueryBuilder<...>
limit(20): IQueryBuilder<...>
list(): QueryResultsPromise<Pick<Transaction, "id" | "accountId" | "amount" | "createdAt">>

First or Null

When you expect a single row, use firstOrNull() or its alias one().

TypeScript
import { eq } from "@onyx.dev/onyx-database";
import { Schema, tables } from "./generated/onyx/types";

const db = onyx.init<Schema>();

const tx = await db
  .from(tables.Transaction)
  .where(eq("id", "txn_123"))
  .firstOrNull();

const txAlt = await db
  .from(tables.Transaction)
  .where(eq("id", "txn_123"))
  .one();

TypeScript return types

from(tables.Transaction): IQueryBuilder<Transaction>
where(eq("id", ...)): IQueryBuilder<Transaction>
firstOrNull(): Promise<Transaction | null>
one(): Promise<Transaction | null>

Inner Queries

Filter a table using sub-selects returned from another query.

TypeScript
import { onyx, within, gt } from "@onyx.dev/onyx-database";
import { Schema, tables } from "./generated/onyx/types";

const db = onyx.init<Schema>();

const accountsWithLargeTx = await db
  .from(tables.Account)
  .where(
    within(
      "id",
      db
        .select("accountId")
        .from(tables.Transaction)
        .where(gt("amount", 1000))
    )
  )
  .list();

TypeScript return types

onyx.init<Schema>(): IOnyxDatabase<Schema>
db.select("accountId"): IQueryBuilder<Transaction>
within("id", subquery): ConditionBuilderImpl
db.from(tables.Account): IQueryBuilder<Account>
where(...): IQueryBuilder<Account>
list(): QueryResultsPromise<Account> // await -> QueryResults<Account> & Account[]

Update Query

Set partial updates for all rows matching a condition.

TypeScript
import { eq, gt } from "@onyx.dev/onyx-database";
import { tables } from "./generated/onyx/types";

const updated = await db
  .from(tables.Transaction)
  .where(eq("status", "pending").and(gt("amount", 100)))
  .setUpdates({ status: "posted" })
  .update();

TypeScript return types

from(tables.Transaction): IQueryBuilder<Transaction>
where(...): IQueryBuilder<Transaction>
setUpdates({ status: "posted" }): IQueryBuilder<Transaction>
update(): Promise<unknown>

Delete Query

Delete all rows that match a filter.

TypeScript
import { eq } from "@onyx.dev/onyx-database";
import { tables } from "./generated/onyx/types";

const deletedCount = await db
  .from(tables.Transaction)
  .where(eq("status", "archived"))
  .delete();

TypeScript return types

from(tables.Transaction): IQueryBuilder<Transaction>
where(eq("status", "archived")): IQueryBuilder<Transaction>
delete(): Promise<number>

Group By

Group results by one or more fields to segment metrics.

TypeScript
import { eq } from "@onyx.dev/onyx-database";
import { tables } from "./generated/onyx/types";

const byMerchant = await db
  .select("merchant", "currency")
  .from(tables.Transaction)
  .where(eq("status", "posted"))
  .groupBy("merchant", "currency")
  .list();

Aggregations

Server-side rollups for dashboards and billing summaries.

TypeScript
import { sum, eq } from "@onyx.dev/onyx-database";
import { tables } from "./generated/onyx/types";

const totals = await db
  .select(sum("amount"), "merchant")
  .from(tables.Transaction)
  .where(eq("status", "posted"))
  .list();

TypeScript return types

from(tables.Transaction): IQueryBuilder<Transaction>
sum("amount"): string // aggregation expression
select(...): IQueryBuilder<Transaction>
groupBy("merchant"): IQueryBuilder<Transaction>
list(): QueryResultsPromise<Record<string, unknown>> // await -> QueryResults<Record<string, unknown>> & Record<string, unknown>[]

Server-side aggregate helpers

FunctionSignatureWhat it does
sumsum("field")Numeric sum of a column.
countcount("field" | "*")Row count; accepts a field or "*".
avgavg("field")Arithmetic mean.
minmin("field")Smallest value.
maxmax("field")Largest value.
medianmedian("field")50th percentile.
percentilepercentile("field", p)p-th percentile; p is 0–100.
stdstd("field")Sample standard deviation.
variancevariance("field")Sample variance.
upperupper("field")Uppercases text for grouping/aggregation.
lowerlower("field")Lowercases text for grouping/aggregation.
formatformat("field", "pattern")Apply a Java-style date/number format before grouping (e.g., yyyy-MM).
substringsubstring("field", from, length)Substring of text (0-based offset).
replacereplace("field", pattern, repl)Regex/substring replacement prior to grouping.
groupBygroupBy(...fields)Groups rows by one or more fields before aggregating.
selectselect(...fields | aggregates)Choose which columns or aggregate expressions to return.
distinctdistinct()Deduplicate rows on the selected fields before aggregation.
resolveresolve(...relations)Resolve related values prior to grouping/aggregating.

Streaming Queries

Consume large result sets incrementally and react to live query responses.

TypeScript
import { onyx, eq } from "@onyx.dev/onyx-database";
import { Schema, tables } from "./generated/onyx/types";

const db = onyx.init<Schema>();

const handle = await db
  .from(tables.Transaction)
  .where(eq("status", "posted"))
  .pageSize(100)
  .onItem((tx, action) => {
    if (!tx) return;
    switch (action) {
      case "QUERY_RESPONSE":
        console.log("Initial result", tx.id, tx.amount);
        break;
      case "CREATE":
        console.log("Created", tx.id);
        break;
      case "UPDATE":
        console.log("Updated", tx.id);
        break;
      case "DELETE":
        console.log("Deleted", tx.id);
        break;
      default:
        break;
    }
  })
  .stream(true, false);

setTimeout(() => handle.cancel(), 1000);

TypeScript return types

from(tables.Transaction): IQueryBuilder<Transaction>
pageSize(100): IQueryBuilder<Transaction>
onItem((tx, action) => ...): IQueryBuilder<Transaction>
stream(includeQueryResults?: boolean, keepAlive?: boolean): Promise<{ cancel(): void }>

Stream behavior

  • Wire format: newline-delimited JSON over PUT /data/{db}/query/stream/{table}; the JS client sets includeQueryResults=true by default, keepAlive=false unless you opt in.
  • includeQueryResults sends the initial snapshot as QUERY_RESPONSE lines; set false to receive only live changes.
  • keepAlive keeps the connection open for future mutations and emits KEEP_ALIVE heartbeats ~every 10s; false closes after the initial batch.
  • Actions delivered: QUERY_RESPONSE, CREATE, UPDATE, DELETE, KEEP_ALIVE.
  • Server flushes frequently (every ~200ms or 32KiB) and runs with Connection: keep-alive; call cancel() on the handle to detach listeners and close cleanly.

Pagination

Page through ordered results with stable cursors (offset-free).

TypeScript
import { desc } from "@onyx.dev/onyx-database";
import { tables } from "./generated/onyx/types";

const page1 = await db
  .from(tables.Transaction)
  .orderBy(desc("createdAt"))
  .list({ pageSize: 50 });

const page2 = page1.nextPage
  ? await db.from(tables.Transaction).nextPage(page1.nextPage).list()
  : null;

if (page2) {
  console.log("page2 size", page2.length, "next token", page2.nextPage);
}

TypeScript return types

from(tables.Transaction): IQueryBuilder<Transaction>
orderBy(...): IQueryBuilder<Transaction>
list({ pageSize: 50 }): QueryResultsPromise<Transaction> // await -> QueryResults<Transaction> & Transaction[]
await list result -> { nextPage?: string }
nextPage(token): IQueryBuilder<Transaction>
list(): QueryResultsPromise<Transaction>

AI Chat

Use the AI endpoint to answer questions that reference your data.

TypeScript

Chat completions

Control model, messages, streaming/raw response options.

const response = await db.ai.chat(
  {
    model: "onyx",
    messages: [
      { role: "system", content: "You are a finance assistant. Respond with 3 concise bullet points." },
      { role: "assistant", content: "I summarize trends and call out anomalies each Friday." },
      { role: "user", content: "Draft a status update for finance stakeholders." },
    ],
    stream: false,
  },
  { raw: true }
);

Streaming chat

Stream tokens as they arrive; remember to consume the async iterator.

const stream = await db.ai.chat(
  {
    model: "onyx",
    messages: [{ role: "user", content: "List the top 3 spend categories this week." }],
    stream: true,
  }
);

for await (const chunk of stream) {
  console.log(chunk.choices[0]?.delta?.content ?? "");
}

Shorthand chat (string in, string out)

Quick, typed call that returns the first message content.

const answer = await db.chat("Summarize yesterday's transactions");

List available models

Discover model IDs before picking one.

const models = await db.ai.getModels();

Get model details

Inspect a specific model's capabilities and limits.

const model = await db.ai.getModel("onyx");

Request script approval

Validate a mutation script before execution.

const approval = await db.ai.requestScriptApproval({
  script: "db.save({ id: 'acct_1', name: 'Checking' })",
});

TypeScript return types

db.chat(text): Promise<string>
db.ai.chat(fullRequest, { raw: true }): Promise<AiChatCompletionResponse | AiChatCompletionStream>
db.ai.chat(text, { stream: true }): Promise<AiChatCompletionStream>
db.ai.chat(fullRequest, { stream: true }): Promise<AiChatCompletionStream>
AiChatCompletionStream: AsyncIterable<AiChatCompletionChunk> & { cancel(): void }
AiChatCompletionChunk.choices[0].delta.content?: string | null
db.ai.getModels(): Promise<AiModelsResponse>
db.ai.getModel(id): Promise<AiModel>
db.ai.requestScriptApproval(input): Promise<AiScriptApprovalResponse>

Documents

Upload binary files with metadata, fetch with optional resizing, or delete stored documents.

TypeScript

Save a document

Stores metadata in the DB and writes bytes to the database’s _documents path.

import { onyx } from "@onyx.dev/onyx-database";
import { Schema } from "./generated/onyx/types";

const db = onyx.init<Schema>();

const documentId = await db.saveDocument({
  path: "/receipts/2026-01-31.pdf",
  mimeType: "application/pdf",
  content: fileBase64,
});

Get a document (optional resize for images)

Fetches by documentId; width/height resize images on the fly.

const doc = await db.getDocument(documentId, {
  width: 800,
  height: 600,
});

Delete a document

Removes the file and its metadata.

await db.deleteDocument(documentId);

TypeScript return types

db.saveDocument(doc): Promise<unknown> // returns generated documentId
db.getDocument(id, { width?, height? }): Promise<unknown>
db.deleteDocument(id): Promise<unknown>

Storage & search

Files are written to your database’s filesystem under _documents/; only metadata (path, mimeType, timestamps) is stored in the Document record. If you provide plaintext content, it is chunked into Lucene-indexed parts so full-text search can find the document.

Secrets

Manage encrypted secrets stored per database; client calls never persist plaintext.

TypeScript

List secrets (metadata only)

Returns keys, purposes, and timestamps—no values.

import { onyx } from "@onyx.dev/onyx-database";
import { Schema } from "./generated/onyx/types";

const db = onyx.init<Schema>();

const { records } = await db.listSecrets();

Fetch a secret value

Decrypts and returns the plaintext for a single key.

const secret = await db.getSecret("stripe_api_key");

Create or rotate a secret

Stores the value encrypted at rest; re-encrypts on each update.

const saved = await db.putSecret("stripe_api_key", {
  value: process.env.STRIPE_KEY ?? "",
  purpose: "Stripe server key",
});

Delete a secret

Removes the record from the database.

await db.deleteSecret("stripe_api_key");

How Onyx secures secrets

  • Each secret is encrypted with a random AES-256-GCM key; that key is wrapped using the database’s 4096-bit RSA public key. Only ciphertext and wrapped keys are stored.
  • Per-database RSA private keys are themselves encrypted with a master key.
  • API routes require valid database credentials; plaintext values are only returned on authenticated GET /secret/{key} and are never persisted decrypted.
  • Back up the keystore directory (secret-keystore) together with the master key to preserve decryption ability.

TypeScript return types

db.listSecrets(): Promise<SecretsListResponse>
db.getSecret(key: string): Promise<SecretRecord>
db.putSecret(key: string, input: SecretSaveRequest): Promise<SecretMetadata>
db.deleteSecret(key: string): Promise<{ key: string }>

Schema

Fetch, diff, validate, and publish schema revisions.

TypeScript
import { onyx, type SchemaEntity } from "@onyx.dev/onyx-database";

const db = onyx.init();

const current = await db.getSchema();

const tempTable: SchemaEntity = {
  name: "TempTable",
  identifier: { name: "id", generator: "UUID", type: "String" },
  attributes: [
    { name: "id", type: "String", isNullable: false },
    { name: "name", type: "String", isNullable: false },
  ],
};

const nextSchema = {
  ...current,
  entities: [...(current.entities ?? []), tempTable],
  revisionDescription: "Add TempTable via SDK",
};

const diff = await db.diffSchema(nextSchema);
const validation = await db.validateSchema(nextSchema);

if (validation.valid) {
  await db.updateSchema(nextSchema, { publish: true });
}

Need the JSON shape for entities, attributes, and resolvers? See Schema Example.

TypeScript return types

db.getSchema(): Promise<SchemaRevision>
db.diffSchema(schema): Promise<SchemaDiff>
db.validateSchema(schema): Promise<SchemaValidationResult>
db.updateSchema(schema, { publish?: boolean }): Promise<SchemaRevision>

Next Steps