npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@onyx.dev/onyx-database

v2.8.2

Published

TypeScript client SDK for Onyx Database

Readme

@onyx.dev/onyx-database

License: MIT codecov npm version

TypeScript client SDK for Onyx Cloud Database — a zero-dependency, strict-typed, builder-pattern API for querying and persisting data in Onyx from Node.js or edge runtimes like Cloudflare Workers. Ships ESM & CJS, includes a credential resolver, and an optional schema code generator that produces table-safe types and a tables enum.


Table of contents


Getting started (Cloud ➜ keys ➜ connect)

  1. Sign up & create resources at https://cloud.onyx.dev
    Create an Organization, then a Database, define your Schema (e.g., User, Role, Permission), and create API Keys.

  2. Note your connection parameters:

    • baseUrl (e.g., https://api.onyx.dev)
    • databaseId
    • apiKey
    • apiSecret
  3. Install the SDK in your project:

    npm i @onyx.dev/onyx-database
  4. Initialize the client using env vars or explicit config.

Supports Node.js 18+ and Cloudflare Workers.


Install

npm i @onyx.dev/onyx-database

The package is dual-module (ESM + CJS) and has no runtime or peer dependencies.

CLI tooling and schema codegen now live in the dedicated Onyx CLI repo: https://github.com/OnyxDevTools/onyx-cli. Install it via the official install script or Homebrew (macOS):

curl -fsSL https://raw.githubusercontent.com/OnyxDevTools/onyx-cli/main/scripts/install.sh | bash

# macOS (Homebrew)
brew tap OnyxDevTools/onyx-cli
brew install onyx-cli

Initialize the client

This SDK resolves credentials automatically using the chain explicit config ➜ environment variables ➜ ONYX_CONFIG_PATH file ➜ project config file ➜ home profile (Node.js only for file-based sources). Call onyx.init({ databaseId: 'database-id' }) to target a specific database, or omit the databaseId to use the default. You can also pass credentials directly via config.

Reliability defaults (read this):

  • Retries: GET/query calls auto-retry up to 3 times with Fibonacci backoff starting at 300ms (honors Retry-After); writes never retry.
  • Config cache: Resolved config is cached per ${databaseId}-${apiKey}-${wireFormat} for 5 minutes; tune with ttl, clear via onyx.clearCacheConfig().

Option A) Environment variables (recommended for production)

Set these environment variables for your database:

| Variable | Purpose | Default when unset | | --- | --- | --- | | ONYX_DATABASE_ID | Optional database scope for env credentials; omit to use file/profile/default context | optional | | ONYX_DATABASE_BASE_URL | Base URL for DB API | https://api.onyx.dev | | ONYX_DATABASE_API_KEY | API key | required | | ONYX_DATABASE_API_SECRET | API secret | required | | ONYX_AI_BASE_URL | Base URL for AI endpoints | https://ai.onyx.dev | | ONYX_DEFAULT_MODEL | Model used by db.chat() shorthand | onyx | | ONYX_CONFIG_PATH | Path to JSON credentials file (Node only; ignored on edge) | unset (falls back to env ➜ project file ➜ home profile) | | ONYX_DEBUG | Enable HTTP + config debug logging | off | | ONYX_STREAM_DEBUG | Enable streaming debug logs | off |

import { onyx } from '@onyx.dev/onyx-database';

const db = onyx.init({ databaseId: 'YOUR_DATABASE_ID' }); // uses env when ID matches
// credentials are cached for 5 minutes by default

Option B) Explicit config

import { onyx } from '@onyx.dev/onyx-database';

const db = onyx.init({
  baseUrl: 'https://api.onyx.dev',
  aiBaseUrl: 'https://ai.onyx.dev', // optional: override AI base path
  defaultModel: 'onyx', // optional: shorthand `db.chat()` model
  databaseId: 'YOUR_DATABASE_ID',
  apiKey: 'YOUR_KEY',
  apiSecret: 'YOUR_SECRET',
  partition: 'tenantA',
  wireFormat: 'json', // optional: opt out of MessagePack for entity routes
  requestLoggingEnabled: true, // logs HTTP requests
  responseLoggingEnabled: true, // logs HTTP responses
});

The partition option sets a default partition for table-scoped queries, findById, and deletes by primary key; database-wide db.search(...) omits it. Save operations use the partition field on the entity itself. Enable requestLoggingEnabled to log each request and its body to the console. Enable responseLoggingEnabled to log responses and bodies. Setting the ONYX_DEBUG=true environment variable enables both request and response logging even if these flags are not set. It also logs the source of resolved credentials (explicit config, env vars, config path file, project file, or home profile).

MessagePack entity transport

MessagePack is the default wire format for entity routes. To use JSON with a legacy deployment that does not support the zero-dependency binary transport, set wireFormat: 'json':

import { onyx } from '@onyx.dev/onyx-database';

const db = onyx.init({
  databaseId: 'YOUR_DATABASE_ID',
  apiKey: 'YOUR_KEY',
  apiSecret: 'YOUR_SECRET',
  wireFormat: 'json',
});

await db.save('User', {
  id: 'user-1',
  profile: { displayName: 'Ada', roles: ['admin'] },
});

const users = await db.from('User').resolve('profile').list();

By default, MessagePack applies only to entity saves, reads, deletes, queries, and query streams. Documents, schemas, secrets, AI, and model-builder requests continue to use JSON. The client sends application/vnd.msgpack and advertises JSON as a lower-priority response fallback; it always decodes the actual response Content-Type, so JSON errors and fallback responses continue to work.

Nested objects, arrays, strings, booleans, nulls, JavaScript-safe numbers, and signed 64-bit bigint values are supported. Dates are normalized to ISO strings as they are for JSON. Integer number inputs must remain within JavaScript's safe range; use bigint for larger signed values. Decoded integers outside the safe range are returned as bigint, while unsigned wire values above 9223372036854775807 are rejected. Because JSON cannot serialize bigint, use these values only with MessagePack (the default), not an explicit wireFormat: 'json' override. Object properties containing undefined are omitted, while undefined array entries and non-finite numbers become null, matching JSON.stringify.

MessagePack streams contain concatenated, self-delimiting values rather than newline framing. The client tolerates values split across network chunks, skips the server's optional initial nil flush frame, and can consume a JSON-lines fallback when indicated by the response content type.

When supplying a custom fetch, request bodies may be string | Uint8Array. Its response object must implement arrayBuffer() when it returns application/vnd.msgpack; JSON-only fetch mocks only need text() when the client is explicitly configured with wireFormat: 'json'. The client does not automatically retry MessagePack mutations as JSON; configure JSON explicitly when targeting an Onyx Cloud deployment without MessagePack support.

Option C) Node-only config files

Set ONYX_CONFIG_PATH to a JSON file containing your credentials. This file is checked after environment variables and before project and home files. When unset, the resolver checks for JSON files matching the OnyxConfig shape in the following order:

  • ./onyx-database-<databaseId>.json
  • ./onyx-database.json
  • ~/.onyx/onyx-database-<databaseId>.json
  • ~/.onyx/onyx-database.json
  • ~/onyx-database.json

These files are ignored in non-Node runtimes like Cloudflare Workers.

Edge / RSC usage (Next.js, Cloudflare Workers)

For edge runtimes (Next.js Edge/RSC, Cloudflare Workers), import the edge entry. It avoids Node-only imports and only resolves credentials from environment variables or explicit config.

import { onyx } from '@onyx.dev/onyx-database/edge';

const db = onyx.init(); // uses env vars in edge runtimes

File-based config (ONYX_CONFIG_PATH, project files, home profiles) is not available in edge runtimes. If you need file-based config, use the Node entry instead.

Cloudflare Worker example:

import { onyx } from '@onyx.dev/onyx-database/edge';

export default {
  async fetch(_request: Request, env: Record<string, string>) {
    const db = onyx.init({
      baseUrl: env.ONYX_DATABASE_BASE_URL,
      databaseId: env.ONYX_DATABASE_ID,
      apiKey: env.ONYX_DATABASE_API_KEY,
      apiSecret: env.ONYX_DATABASE_API_SECRET,
    });

    return Response.json({ ok: true });
  },
};

Connection & config caching

Calling onyx.init() returns a lightweight client. Configuration is resolved once and cached per ${databaseId}-${apiKey}-${wireFormat} tuple for 5 minutes to avoid repeated env/file lookups (override with ttl or reset via onyx.clearCacheConfig()). Each database instance keeps a single internal HttpClient. Requests use the runtime's global fetch, which already reuses connections and pools them for keep‑alive. Reuse the returned db for multiple operations; extra SDK‑level connection pooling generally isn't necessary unless you create many short‑lived clients.

Typed vs untyped init

onyx.init() is generic. Omit the type for quick scripts; add your generated schema type for full safety.

// Untyped: flexible, no compile-time field checks
const db = onyx.init();
const user = await db.from('User').findById('abc'); // inferred as any/unknown
// Typed: import generated schema to get autocomplete and validation
import type { OnyxSchema as Schema } from './onyx/types';
const db = onyx.init<Schema>();
const user = await db.from('User').findById('abc'); // inferred as Schema['User']
// user.emali -> TypeScript error

Onyx AI (chat & models)

AI endpoints are OpenAI-compatible and use the same credentials as database calls. Use db.ai for chat, models, and script approvals; db.chat()/db.chat('...') remain supported as equivalent entrypoints. A shorthand db.chat('content') call is available and uses config.defaultModel (defaults to onyx). The AI base URL defaults to https://ai.onyx.dev and can be overridden with aiBaseUrl (or ONYX_AI_BASE_URL). The databaseId query param is optional; when omitted, the configured databaseId is used for grounding and billing.

Chat completions

Examples: examples/ai/chat.ts, examples/ai/chat-stream.ts.

import { onyx } from '@onyx.dev/onyx-database';

const db = onyx.init();

const quick = await db.chat('Reply with exactly one short greeting sentence.'); // returns first message content

const completion = await db.ai.chat({
  model: 'onyx-chat',
  messages: [{ role: 'user', content: 'Summarize last week’s traffic.' }],
});
console.log(completion.choices[0]?.message?.content);

// Override defaults (model/role/temperature/stream) in shorthand form
const custom = await db.chat('List three colors.', {
  model: 'onyx-chat',
  role: 'user',
  temperature: 0.2,
  stream: false, // set raw: true to receive full completion response instead of the first message content
});

Streaming works as an async iterable:

const stream = await db.ai.chat({
  model: 'onyx-chat',
  stream: true,
  messages: [{ role: 'user', content: 'Write a short onboarding checklist.' }],
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
}
// stream.cancel() is available if you need to stop early

Tool calls mirror the ChatGPT TypeScript client:

const prompt = {
  model: 'onyx-chat',
  messages: [{ role: 'user', content: 'Find revenue for ACME in 2023.' }],
  tools: [
    {
      type: 'function',
      function: {
        name: 'get_revenue',
        description: 'Fetch revenue for a company and year',
        parameters: {
          type: 'object',
          properties: {
            company: { type: 'string' },
            year: { type: 'number' },
          },
          required: ['company', 'year'],
        },
      },
    },
  ],
};

const first = await db.ai.chat(prompt);
const toolCall = first.choices[0]?.message?.tool_calls?.[0];

if (toolCall) {
  const toolResult = await getRevenue(JSON.parse(toolCall.function.arguments)); // your impl
  const followup = await db.ai.chat({
    model: prompt.model,
    messages: [
      ...prompt.messages,
      first.choices[0].message,
      { role: 'tool', tool_call_id: toolCall.id ?? '', content: JSON.stringify(toolResult) },
    ],
  });
  console.log(followup.choices[0]?.message?.content);
}

Model metadata

Example: examples/ai/models.ts.

const models = await db.ai.getModels();
const chatModel = await db.ai.getModel('onyx-chat');

Script mutation approvals

const approval = await db.ai.requestScriptApproval({
  script: "db.save({ id: 'u1', email: '[email protected]' })",
});
if (approval.requiresApproval) {
  console.log(`Requires approval until ${approval.expiresAtIso}`);
}

Published model predictions

Use a published model key with raw input rows, or let a saved script provide the input rows for prediction.

const rawPrediction = await db.predict('churn-model', [
  { age: 42, country: 'US', usageScore: 0.87 },
  { age: 31, country: 'CA', usageScore: 0.42 },
]);

console.log(rawPrediction.predictions);
console.log(rawPrediction.rawPredictions);

const scriptPrediction = await db.predictFromScript(
  'churn-model',
  'score-active-users',
  { segment: 'enterprise' },
);

console.log(scriptPrediction.inputCount);

Optional: generate TypeScript types from your schema

Use the Onyx CLI (onyx) from https://github.com/OnyxDevTools/onyx-cli to emit per-table interfaces, a tables enum, and a Schema mapping for compile-time safety and IntelliSense. Each generated interface also includes an index signature so extra properties (for graph attachments in cascade saves) don't trigger type errors.

Generate directly from the API (using the same credential resolver as init()):

onyx gen --ts --source api --out ./src/onyx/types.ts --name OnyxSchema

With --source api, onyx gen calls the Schema API (same as onyx schema get) using the standard config chain (env, project file, home profile).

Timestamp attributes are emitted as Date fields by default. When saving, Date values are automatically serialized to ISO timestamp strings. Pass --timestamps string to keep timestamps as ISO strings in generated types.

Or from a local schema file you export from the console:

onyx gen --ts --source file --schema ./onyx.schema.json --out ./src/onyx/types.ts --name OnyxSchema

Run it with no flags to use the defaults: onyx gen reads ./onyx.schema.json and writes to ./onyx/types.ts.

Manage schemas from the CLI

Publish or download schema JSON directly via API using the onyx schema helper:

# Publish ./onyx.schema.json with publish=true by default
onyx schema publish

# Overwrite ./onyx.schema.json with the remote schema
onyx schema get

# Print the remote schema without writing a file
onyx schema get --print

# Fetch only selected tables (prints to stdout; does not overwrite files)
onyx schema get --tables=User,Profile

# Example subset output
onyx schema get --tables=User,Profile
# {
#   "tables": [
#     {
#       "name": "User",
#       "attributes": [
#         { "name": "id", "type": "string", "required": true },
#         { "name": "email", "type": "string", "required": true }
#       ]
#     },
#     {
#       "name": "Profile",
#       "attributes": [
#         { "name": "id", "type": "string", "required": true },
#         { "name": "userId", "type": "string", "required": true }
#       ]
#     }
#   ]
# }

# Validate a schema file without publishing
onyx schema validate ./onyx.schema.json

# Diff local schema vs API
onyx schema diff ./onyx.schema.json
# Prints YAML with added/removed/changed tables and attribute differences between the API schema and your local file.

When --tables is provided, the subset is printed to stdout instead of writing a file. Otherwise, the CLI writes to ./onyx.schema.json by default.

In this repo's examples/ workspace, the following scripts wrap the same commands:

npm run schema:get       # fetch remote schema into ./onyx.schema.json
npm run schema:validate  # validate the local schema file
npm run schema:publish   # validate then publish the local schema

The CLI reuses the same configuration resolution as onyx.init() (env vars, project config, and home profile files).

Programmatic diffing is also available:

import { onyx } from '@onyx.dev/onyx-database';

const db = onyx.init();
const diff = await db.diffSchema(localSchema); // SchemaUpsertRequest
console.log(diff.changedTables);

You can also emit to multiple paths in one run (comma-separated or by repeating --out):

onyx gen --ts --out ./src/onyx/types.ts,./apps/admin/src/onyx/types.ts

Use in code:

import { onyx, eq, asc } from '@onyx.dev/onyx-database';
import { tables, Schema } from './src/onyx/types';

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

const User = await db
  .from(tables.User)
  .where(eq('status', 'active'))
  .orderBy(asc('createdAt'))
  .limit(20)
  .list();

For a schema with User, UserProfile, Role, and Permission tables, onyx gen emits plain interfaces keyed by IDs. Each interface includes an index signature so resolver-attached fields or embedded objects remain type-safe:

// AUTO-GENERATED BY onyx gen. DO NOT EDIT.
export interface User {
  id?: string;
  name: string;
  [key: string]: any;
}

export interface Role {
  id?: string;
  title: "",
  [key: string]: any;
}

export interface Permission {
  id?: string;
  description: string;
  [key: string]: any;
}

export interface UserProfile {
  id?: string;
  age: number;
  [key: string]: any;
  createdDate: Date;
}

const user = await db
  .from(tables.User)
  .select('id', 'username')
  .resolve('roles.permissions', 'profile')
  .firstOrNull();

// user.roles -> Role[]
// user.roles[0]?.permissions -> Permission[]
// user.profile -> UserProfile | undefined

The generator defaults to not emitting the JSON copy of your schema. Use --emit-json if you want it.

Modeling users, roles, and permissions

User and Role form a many-to-many relationship through a UserRole join table. Role and Permission are connected the same way via RolePermission.

  • userRoles / rolePermissions resolvers return join-table rows. Use these when cascading saves or deletes to add or remove associations.
  • roles / permissions resolvers traverse those joins and return Role or Permission records for display.

Define these resolvers in your onyx.schema.json:

"resolvers": [
  {
    "name": "roles",
    "resolver": "db.from(\"Role\")\n  .where(\n    inOp(\"id\", \n        db.from(\"UserRole\")\n            .where(eq(\"userId\", this.id))\n            .list()\n            .values('roleId')\n    )\n)\n .list()"
  },
  {
    "name": "profile",
    "resolver": "db.from(\"UserProfile\")\n .where(eq(\"userId\", this.id))\n .firstOrNull()"
  },
  {
    "name": "userRoles",
    "resolver": "db.from(\"UserRole\")\n  .where(eq(\"userId\", this.id))\n  .list()"
  }
]

Save a user and attach roles in one operation:

await db.cascade('userRoles:UserRole(userId, id)').save('User', {
  id: 'user_126',
  email: '[email protected]',
  userRoles: [
    { roleId: 'role_admin' },
    { roleId: 'role_editor' },
  ],
});

Fetch a user with roles and each role's permissions:

const detailed = await db
  .from('User')
  .resolve('roles.permissions', 'profile')
  .firstOrNull();

// detailed.roles -> Role[]
// detailed.roles[0]?.permissions -> Permission[]

Remove a role and its permission links:

await db.cascade('rolePermissions').delete('Role', 'role_temp');

Query helpers at a glance

Importable helpers for conditions and sort:

import {
  eq, neq, within, notWithin, // preferred aliases for IN/NOT IN
  inOp, notIn,           
  between,
  gt, gte, lt, lte,
  like, notLike, contains, notContains,
  startsWith, notStartsWith, matches, notMatches, search,
  approximateSearch, hnswCandidates, approximateCandidates,
  isNull, notNull,
  asc, desc
} from '@onyx.dev/onyx-database';
  • Prefer within/notWithin for inclusion checks (supports arrays, comma-separated strings, or inner queries).
  • inOp/notIn remain available for backward compatibility and are exact aliases.
  • search(text, minScore?) keeps the native vector-managed MATCHES predicate on __full_text__ and always serializes minScore (null when omitted).
  • search(text, { mode, ... }) builds the high-level SEARCH predicate for lexical, semantic, or hybrid retrieval. An empty options object defaults to hybrid mode and match: "any".
  • approximateSearch, hnswCandidates, and approximateCandidates build physically bounded, read-only candidate criteria. Pass the ordinary-index helper to where(approximateCandidates(...)), just like where(eq(...)). approximateSearch and hnswCandidates must be the sole root criterion. One approximateCandidates criterion may be combined with non-negated AND filters; query builders reject OR, negation, duplicate candidate admission, and update/delete execution before transport.

Aggregate helpers

import {
  avg, sum, count, min, max,
  std, variance, median,
  upper, lower,
  substring, replace, percentile,
  format
} from '@onyx.dev/onyx-database';

const rows = await db
  .select(format('createdAt', 'yyyy-MM-dd'))
  .from('User')
  .list();
  • format(field, formatter) uses Java-style format strings for dates and numbers.
  • Example: examples/query/format.ts

Inner queries (IN/NOT IN with sub-selects)

You can pass another query builder to within or notWithin to create nested filters. The SDK serializes the inner query (including its table) before sending the request.

import { onyx, within, notWithin, eq, tables, Schema } from '@onyx.dev/onyx-database';

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

// Users that HAVE the admin role
const usersWithAdmin = await db
  .from(tables.User)
  .where(
    within(
      'id',
      db.select('userId').from(tables.UserRole).where(eq('roleId', 'role-admin')),
    ),
  )
  .list();

// Roles that DO NOT include a specific permission
const rolesMissingPermission = await db
  .from(tables.Role)
  .where(
    notWithin(
      'id',
      db.from(tables.RolePermission).where(eq('permissionId', 'perm-manage-users')),
    ),
  )
  .list();

Native vector-managed and bounded candidate search

Use .search(text, options) on a query builder for high-level natural-language search, or call db.search(text, options) to target all tables (table = "ALL" in the request body). The options select lexical, semantic, or hybrid retrieval; {} selects hybrid with all canonical defaults.

The existing one-argument .search(text) and numeric .search(text, minScore) forms remain the legacy MATCHES API for compatibility. The legacy value always includes minScore and falls back to null when omitted.

import {
  desc,
  eq,
  onyx,
  search,
  semanticVectorSignature,
  tables,
  type Schema,
} from '@onyx.dev/onyx-database';

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

// Table-specific search with a minimum score
const recentUsers = await db
  .from(tables.User)
  .search('user bio text', 4.4)
  .orderBy(desc('createdAt'))
  .limit(5)
  .list();

// Search across all tables (table: "ALL")
const acrossTables = await db.search('user bio text').list({ pageSize: 5 });

// Combine a search predicate with other filters
const activeMatch = await db
  .from(tables.User)
  .where(search('user bio text'))
  .and(eq('isActive', true))
  .firstOrNull();

// Natural-language lexical search. Extra question words do not prevent a match
// when any terms clear the requested score threshold.
const lexical = await db
  .from('ActiveDocumentChunk')
  .search('how do i calculate cost per horse', {
    mode: 'lexical',
    match: 'any',
    minScore: 0.4,
    maxCandidates: 500,
  })
  .list();

// Server-managed semantic search from ordinary text.
const semanticMatches = await db
  .from('ActiveDocumentChunk')
  .search('how do i calculate cost per horse', { mode: 'semantic' })
  .list();

// Fuse lexical and semantic retrieval with the same clean API.
const hybrid = await db
  .from('ActiveDocumentChunk')
  .search('how do i calculate cost per horse', {}) // mode defaults to hybrid
  .and(eq('isActive', true))
  .list();

// Semantic or hybrid MATCHES search. The helper validates the routing signature
// and preserves each 64-bit identifier/fingerprint word losslessly on the wire.
const semantic = semanticVectorSignature({
  calibrationId: 73n,
  bucketId: 6,
  cells: [1, 2],
  cellCounts: [4, 4],
  fingerprint: ['0xfedcba9876543210'],
  boundaryConfidence: 0.75,
});
const hybridMatches = await db
  .from('ActiveDocumentChunk')
  .search({
    text: 'customer success',
    semantic,
    minScore: 0.42,
    nearbyBucketRadius: 2,
    maxCandidates: 321,
    requireAllTerms: false,
  })
  .list();

// Bounded lexical admission. For a partitioned table, select one partition.
const lexicalCandidates = await db
  .from('ActiveDocumentChunk')
  .approximateSearch('customer success', { maxCandidates: 128 })
  .inPartition('revision-7')
  .limit(20)
  .list();

// Native HNSW admission. The calibration id is text to preserve signed int64 values.
const semanticCandidates = await db
  .from('ChunkAttentionHash')
  .hnswCandidates({
    calibrationId: '73',
    vector: promptEmbedding,
    maxCandidates: 256,
    efSearch: 1024,
  })
  .inPartition('revision-7')
  .limit(20)
  .list();

// Bounded admission from one ordinary secondary index.
const hashCandidates = await db
  .from('ChunkAttentionHash')
  .where(approximateCandidates('bucketId', [1201, 1202, 1203], 1024))
  .and(eq('active', true))
  .and(eq('corpusId', 'corpus-a'))
  .inPartition('revision-7')
  .list();

Candidate helpers enforce the public server bounds: at most 5,000 admitted rows, at most 20,000 HNSW distance evaluations, at most 16,384 vector dimensions, and at most 5,000 ordinary-index route values. Candidate results are approximate; rerank them when exact ordering matters. One CANDIDATES condition may be combined with ordinary predicates through AND in either call order. The server admits the bounded route once and evaluates all predicates only over that set; OR trees remain invalid because they could expand beyond the admission bound.

Omitted vector-search options use the server contract defaults: nearbyBucketRadius = 1, maxCandidates = 1000, and requireAllTerms = true. HNSW defaults to maxCandidates = 1000, efSearch = max(1000, maxCandidates), minScore = null, and formatVersion = 1. SEARCH_CANDIDATES is deliberately text-only; semantic and hybrid searches use MATCHES through .search({...}).

The high-level options overload emits a dedicated SEARCH criterion. Its canonical defaults are mode = "hybrid", match = "any", minScore = null, and maxCandidates = 1000; a non-null high-level score must be between 0 and 1 inclusive. Hybrid search requires at least two candidates so both retrieval channels can run; lexical-only and semantic-only search allow one. SEARCH is read-only and can be combined with ordinary structured filters. It may appear only once and cannot be combined with another __full_text__ predicate. The lower-level SEARCH_CANDIDATES and HNSW_CANDIDATES operators remain sole-root operations. High-level and candidate-admission queries cannot be used as live query streams. Existing one-argument .search(text), .search(text, minScore), and .search(VectorSearchQueryInput) calls retain their original MATCHES wire format.

A table-scoped high-level search spans that table's current partitions by default under one global candidate budget; call .inPartition(...) to constrain it. Low-level candidate APIs still require one concrete partition. Database-wide db.search(text, options) searches eligible unpartitioned tables only, never inherits the client's default partition, and returns typed FullTextSearchResult envelopes with id, entityType, entity, and a normalized nullable score.

Semantic and hybrid modes require the database server to have a search embedding provider configured. Stored searchable text and query text must be embedded with the same model, calibration, and vector space. Records written before that integration was enabled need to be resaved or backfilled so their HNSW vectors exist; marking a table SEARCHABLE alone does not retroactively embed existing records.

Examples

  • Table search (minScore null): examples/query/vector-table-search.ts
  • Table search (minScore 4.4): examples/query/vector-table-search-min-score.ts
  • ALL tables search (minScore null): examples/query/vector-search-all-tables.ts
  • ALL tables search (minScore 4.4): examples/query/vector-search-all-tables-min-score.ts

Usage examples with User, Role, Permission

The examples assume your schema has tables named User, Role, and Permission.
If you generated types, replace string literals with tables.User, tables.Role, etc., and type your results with Schema.

1) List (query & paging)

import { onyx, eq, contains, asc } from '@onyx.dev/onyx-database';

const db = onyx.init();

// Fetch first 25 active User whose email contains "@example.com"
const firstPage = await db
  .from('User')
  .where(eq('status', 'active'))
  .and(contains('email', '@example.com'))
  .orderBy(asc('createdAt'))
  .limit(25)
  .page(); // or .list() for array-like results with nextPage

// Iterate to fetch all pages:
const allActive = await db
  .from('User')
  .where(eq('status', 'active'))
  .list();

// Collect IDs across all pages
const ids = await db.from('User').list().values('id');
// Get the first user across pages
const firstUser = await db.from('User').list().firstOrNull();
// Call any QueryResults helper before awaiting
const size = await db.from('User').list().size();

1b) First or null

const maybeUser = await db
  .from('User')
  .where(eq('email', '[email protected]')) // avoid searching by indentifier with firstOrNull, it will throw not found error
  .firstOrNull(); // or .one()

1c) Terminal formatters

const table = await db.from('User').select('id', 'email').table();
const tree = await db.from('User').select('id', 'email').tree({ rootLabel: 'users' });
const csv = await db.from('User').select('id', 'email').csv({ headers: false });
const json = await db.from('User').select('id', 'email').json();

console.log(table);
console.log(tree);
console.log(csv);
console.log(json);

table() renders readable box-drawing output, tree() expands nested objects hierarchically, csv() flattens nested objects with dot notation by default, and json() preserves the original nested structure.

2) Save (create/update)

import { onyx } from '@onyx.dev/onyx-database';
const db = onyx.init();

// Upsert a single user
await db.save('User', {
  id: 'user_123',
  email: '[email protected]',
  status: 'active',
});

// Batch upsert User
await db.save('User', [
  { id: 'user_124', email: '[email protected]', status: 'active' },
  { id: 'user_125', email: '[email protected]', status: 'invited' },
]);

// Save many users in batches of 500
await db.batchSave('User', largeUserArray, 500);

// Save with cascade relationships (example)
await db.cascade('userRoles:UserRole(userId, id)').save('User', {
  id: 'user_126',
  email: '[email protected]',
  userRoles: [
    { roleId: 'role_admin' },
    { roleId: 'role_editor' },
  ],
});

// Cascade relationship syntax:
// field:Type(target, source)
//   field  – property or path relative to the entity being saved
//   Type   – related table name
//   target – foreign key on the related table
//   source – field on the top level entity used as the key

// Using the CascadeRelationshipBuilder
const permission = { id: 'perm_edit_content', description: 'Edit content' };
const permissionsCascade = db
  .cascadeBuilder()
  .graph('permissions')
  .graphType('Permission')
  .targetField('roleId')
  .sourceField('id');

await db.cascade(permissionsCascade).save('Role', {
  id: 'role_editor',
  name: 'Editor',
  permissions: [permission],
});

3) Delete (by primary key)

import { onyx } from '@onyx.dev/onyx-database';
const db = onyx.init();

// Simple delete returns true when the request succeeds
const deleted = await db.delete('User', 'user_125');

// Delete cascading relationships (example)
await db.delete('Role', 'role_temp', { relationships: ['rolePermissions'] });
// this will delete all of the related permissions that come back from the rolePermissions resolver
// builder pattern equivalent
await db.cascade('rolePermissions').delete('Role', 'role_temp');

4) Delete using query

import { onyx } from '@onyx.dev/onyx-database';
const db = onyx.init();

const delCount = await db
  .from(tables.User)
  .where(eq('status', 'inactive'))
  .delete();
//this will delete all inactive users in the system

5) Schema API

import { onyx } from '@onyx.dev/onyx-database';
const db = onyx.init();

// Fetch current schema (optionally filter by tables)
const schema = await db.getSchema({ tables: ['User', 'Profile'] });

// Review history
const history = await db.getSchemaHistory();

// Validate changes without applying
await db.validateSchema({
  revisionDescription: 'Add profile triggers',
  entities: [
    {
      name: 'Profile',
      identifier: { name: 'id', generator: 'UUID' },
      attributes: [
        { name: 'id', type: 'String', isNullable: false },
        { name: 'userId', type: 'String', isNullable: false },
      ],
    },
  ],
});

// Update and optionally publish
await db.updateSchema(
  {
    revisionDescription: 'Publish profile changes',
    entities: [
      {
        name: 'Profile',
        type: 'SEARCHABLE',
        // LEXICAL, SEMANTIC, or BOTH (the backward-compatible default)
        searchSupport: 'BOTH',
        identifier: { name: 'id', generator: 'UUID' },
        attributes: [
          { name: 'id', type: 'String', isNullable: false },
          { name: 'userId', type: 'String', isNullable: false },
        ],
      },
    ],
  },
  { publish: true },
);

searchSupport controls which indexes a SEARCHABLE entity maintains and therefore which high-level search modes it accepts. Use LEXICAL for term matching, SEMANTIC for embedding/HNSW retrieval, or BOTH to allow lexical, semantic, and hybrid queries. The field is ignored for DEFAULT entities. Changing it rebuilds the entity's search indexes; existing schema JSON without the field behaves as BOTH.

6) Secrets API

import { onyx } from '@onyx.dev/onyx-database';
const db = onyx.init();

// List secret metadata
const list = await db.listSecrets();

// Read a decrypted secret value
const secret = await db.getSecret('api-key');

// Create or update a secret
await db.putSecret('api-key', {
  value: 'super-secret',
  purpose: 'Access to external API',
});

// Delete a secret
await db.deleteSecret('api-key');

7) Documents API (binary assets)

import { onyx, type OnyxDocument } from '@onyx.dev/onyx-database';
const db = onyx.init();

// Save / upload a document (Base64 content)
const logoPng = Buffer.from('89504E47...', 'hex').toString('base64');
const doc: OnyxDocument = {
  documentId: 'logo.png',
  path: '/brand/logo.png',
  mimeType: 'image/png',
  content: logoPng,
};
await db.saveDocument(doc);

// Get a document (optionally with resizing hints if supported)
const image = await db.getDocument('logo.png', { width: 128, height: 128 });

// Delete a document
await db.deleteDocument('logo.png');

8) Streaming (live changes)

import { onyx, eq } from '@onyx.dev/onyx-database';
const db = onyx.init();

const stream = db
  .from('User')
  .where(eq('status', 'active'))
  .onItemAdded((u) => console.log('USER ADDED', u))
  .onItemUpdated((u) => console.log('USER UPDATED', u))
  .onItemDeleted((u) => console.log('USER DELETED', u))
  .onItem((entity, action) => console.log('STREAM EVENT', action, entity));

// Start the stream and keep the connection alive for new events:
const handle = await stream.stream(true, true);

// Later, cancel:
setTimeout(() => handle.cancel(), 60_000);

Debugging: set ONYX_STREAM_DEBUG=1 to log stream connection details.


Error handling

  • OnyxConfigError – thrown by init() if required connection parameters are missing.
  • HttpError – thrown for non-2xx API responses, with status and message from the server.

Use standard try/catch or .catch() patterns:

try {
  const db = onyx.init();
  // ...perform queries...
} catch (err) {
  console.error('Onyx error:', err);
  // Handle configuration or HTTP errors here.
}

HTTP retries

  • GET requests retry automatically with Fibonacci backoff (300ms base) up to 3 times by default; mutations are never retried.
  • Disable or tune via retry on onyx.init:
const db = onyx.init({
  retry: {
    enabled: true,     // default
    maxRetries: 2,     // default 3
    initialDelayMs: 500, // default 300
  },
});

Onyx CLI

+----------------------+-----------------------------------------------+--------------------------------------------------------------+
| Command              | Flags                                         | Defaults / notes                                             |
+----------------------+-----------------------------------------------+--------------------------------------------------------------+
| onyx gen             | --ts/--typescript                             | Default: --source file; --schema ./onyx.schema.json;         |
|                      | --source auto|api|file                        | --out ./onyx/types.ts (file or dir; repeatable);             |
|                      | --schema <path>                               | schema type name: OnyxSchema; timestamps default: date.      |
|                      | --out <dir|file>                              | Use --overwrite to force output; quiet=false.                |
|                      | --name <T>                                    |                                                              |
|                      | --timestamps string|date|number               |                                                              |
|                      | --overwrite / --no-overwrite                  |                                                              |
|                      | -q / --quiet                                  |                                                              |
|                      | -h / --help                                   |                                                              |
+----------------------+-----------------------------------------------+--------------------------------------------------------------+
| onyx schema get      | [file] (positional)                           | Default file: ./onyx.schema.json; writes file unless         |
|                      | --tables a,b                                  | --tables or --print (then prints to stdout).                 |
|                      | --print                                       |                                                              |
|                      | -h / --help                                   |                                                              |
+----------------------+-----------------------------------------------+--------------------------------------------------------------+
| onyx schema publish  | [file] (positional)                           | Default file: ./onyx.schema.json; validates before publishing; |
|                      | -h / --help                                   | uses onyx.init credential resolver.                          |
+----------------------+-----------------------------------------------+--------------------------------------------------------------+
| onyx schema validate | [file] (positional)                           | Default file: ./onyx.schema.json; exits non-zero on errors.  |
|                      | -h / --help                                   |                                                              |
+----------------------+-----------------------------------------------+--------------------------------------------------------------+
| onyx schema diff     | [file] (positional)                           | Default file: ./onyx.schema.json; prints YAML diff vs API.   |
|                      | -h / --help                                   |                                                              |
+----------------------+-----------------------------------------------+--------------------------------------------------------------+
| onyx schema info     | -h / --help                                   | Shows resolved config sources, config path, connection check.|
+----------------------+-----------------------------------------------+--------------------------------------------------------------+

Runtime & bundlers

  • ESM: dist/index.js
  • CJS: dist/index.cjs
  • Types: dist/index.d.ts

Works in Node 18+ and modern bundlers (Vite, esbuild, Webpack). For TypeScript, prefer:

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "strict": true
  }
}

Release workflow

This repository uses Changesets for versioning and publishing.

  1. Run npm run changeset to create a changeset entry.
  2. Push to main and the Release workflow opens a version PR.
  3. Push the version commit and create a v* tag to trigger the publish job.
  4. The publish job uses npm trusted publishing from GitHub Actions (OIDC), so npm must be configured with a trusted publisher for OnyxDevTools/onyx-database and the Release workflow file.

Related links


Security

See SECURITY.md for our security policy and vulnerability reporting process.


License

MIT © Onyx Dev Tools. See LICENSE.


Keywords: Onyx Database TypeScript SDK, Onyx Cloud Database, Onyx NoSQL Graph Database client, TypeScript query builder, tables enum, schema code generation, zero-dependency database client, ESM CJS, Node.js database SDK, User Role Permission example