Skip to main content

JavaScript / Node.js SDK Guide

Skill Level: Beginner Time Required: 30 minutes Requirements: Node.js 18+, TypeScript 5+ (optional) Package: @sochdb/sochdb v0.5.3 (Apache-2.0)

Complete guide to SochDB's Node.js / TypeScript SDK: an embedded FFI engine plus gRPC and IPC clients, with namespaces, vector collections, priority queues, semantic cache, a memory system, MCP integration, and policy services.

License

The Node.js SDK is licensed Apache-2.0. The SochDB core engine it links against (the Rust workspace and the sochdb crate) is AGPL-3.0-or-later with commercial licensing available. The Python and Go SDKs are likewise Apache-2.0; only the core engine is AGPL.


Installation

npm install @sochdb/sochdb
# or
yarn add @sochdb/sochdb
# or
pnpm add @sochdb/sochdb

Requires Node.js >=18.0.0. The package bundles native binaries (loaded via koffi FFI) and full TypeScript definitions.

Runtime dependencies: @grpc/grpc-js, @grpc/proto-loader, koffi, uuid. Optional posthog-node for analytics.

CLI tools shipped with the package: sochdb-server, sochdb-bulk, sochdb-grpc-server.

Windows support

The embedded FFI engine works on all major platforms, but the IPC client and the embedded server manager use Unix domain sockets and are not available on Windows. The gRPC client (SochDBClient) works everywhere.


Quick Start

The default and recommended path is the embedded engine. EmbeddedDatabase is exported as Database for convenience.

open() is synchronous

EmbeddedDatabase.open() (and the top-level open()) return an EmbeddedDatabase synchronously — they are not promises. The KV operations on the returned instance (put/get/delete/etc.) are async. You may write await Database.open(...), but the await does nothing.

import { Database } from '@sochdb/sochdb';

async function main() {
// open() is SYNCHRONOUS — no await needed
const db = Database.open('./my_database');

try {
// Keys and values are always Buffers
await db.put(Buffer.from('user:123'), Buffer.from('{"name":"Alice","age":30}'));

const value = await db.get(Buffer.from('user:123'));
console.log(value?.toString());
// {"name":"Alice","age":30}
} finally {
db.close(); // close() is synchronous and returns void
}
}

main();

Deployment modes

The SDK supports three ways to talk to SochDB.

1. Embedded mode (FFI)

Direct FFI bindings to the Rust engine. No server process required. This is the primary mode for most applications.

import { Database, open } from '@sochdb/sochdb';

// Either the class static or the top-level helper
const db = Database.open('./mydb');
// const db = open('./mydb');

await db.put(Buffer.from('key'), Buffer.from('value'));
db.close();

open() accepts an optional EmbeddedDatabaseConfig:

import { open, EmbeddedDatabaseConfig } from '@sochdb/sochdb';

const config: EmbeddedDatabaseConfig = {
walEnabled: true,
syncMode: 'normal', // 'full' | 'normal' | 'off'
memtableSizeBytes: 64 * 1024 * 1024,
groupCommit: true,
indexPolicy: 'balanced', // 'write_optimized' | 'balanced' | 'scan_optimized' | 'append_only'
};

const db = open('./mydb', config);

2. Concurrent mode (FFI + MVCC)

Multi-process access with MVCC — for PM2 clusters, multiple Express workers, etc. Requires the native library >=0.4.8.

import { openConcurrent } from '@sochdb/sochdb';

// Throws if the native lib is too old, unless you opt into fallback
const db = openConcurrent('./shared_db', { fallbackToStandard: true });
console.log(`Concurrent: ${db.isConcurrent}`); // true if supported
console.log(`Fell back: ${db.isConcurrentFallback}`); // true if downgraded

You can probe support up front with the static method:

import { EmbeddedDatabase } from '@sochdb/sochdb';

if (EmbeddedDatabase.isConcurrentModeAvailable()) {
const db = EmbeddedDatabase.openConcurrent('./shared_db');
}

3. Server mode (gRPC)

A thin client connecting to a sochdb-grpc-server. Works on all platforms.

import { SochDBClient } from '@sochdb/sochdb';

const client = new SochDBClient({ address: 'localhost:50051' });

// Signature: put(key, value, namespace?, ttlSeconds?)
await client.put(Buffer.from('key'), Buffer.from('value'));
const value = await client.get(Buffer.from('key')); // get(key, namespace?)

client.close();

SochDBClient is also exported under the alias GrpcClient. See Server mode for the full surface.

There is no routing module

Earlier drafts referenced a client-side routing module. No such module exists in the Node.js SDK. Choose a mode explicitly via the constructors above.


Core key-value operations

EmbeddedDatabase requires Buffer keys and values. Each top-level KV call runs inside its own auto-committed transaction.

const db = Database.open('./my_db');

// Put
await db.put(Buffer.from('key'), Buffer.from('value'));

// Get — returns Buffer | null
const value = await db.get(Buffer.from('key'));
console.log(value?.toString()); // "value"

// Delete
await db.delete(Buffer.from('key'));
console.log(await db.get(Buffer.from('key'))); // null

JSON helper pattern

const user = { name: 'Alice', email: 'alice@example.com', age: 30 };
await db.put(Buffer.from('users/alice'), Buffer.from(JSON.stringify(user)));

const raw = await db.get(Buffer.from('users/alice'));
if (raw) {
const retrieved = JSON.parse(raw.toString());
console.log(`Name: ${retrieved.name}, Age: ${retrieved.age}`);
}

Path API

Path operations store and read values under a structured key path.

await db.putPath('users/alice/email', Buffer.from('alice@example.com'));
await db.putPath('users/alice/settings/theme', Buffer.from('dark'));

const email = await db.getPath('users/alice/email');
console.log(email?.toString()); // alice@example.com

Prefix scanning

scanPrefix(prefix) is an AsyncGenerator<[Buffer, Buffer]>. It opens a read transaction internally, commits on completion, and aborts on error. The native iterator frees memory and closes in a finally block.

await db.put(Buffer.from('tenants/acme/users/1'), Buffer.from('{"name":"Alice"}'));
await db.put(Buffer.from('tenants/acme/users/2'), Buffer.from('{"name":"Bob"}'));
await db.put(Buffer.from('tenants/globex/users/1'), Buffer.from('{"name":"Charlie"}'));

const acme: Array<[string, string]> = [];
for await (const [key, value] of db.scanPrefix(Buffer.from('tenants/acme/'))) {
acme.push([key.toString(), value.toString()]);
}

console.log(`ACME has ${acme.length} items`);
Single-prefix scan

scanPrefix takes one prefix argument and matches every key that starts with it — you do not pass a start/end range. Use a trailing separator (e.g. tenants/acme/) to scope the scan precisely.

Maintenance

const lsn = await db.checkpoint();       // Promise<bigint> — returns the checkpoint LSN
const stats = await db.stats(); // memtable/WAL sizes, active txns, snapshots, last checkpoint LSN
console.log(stats.memtableSizeBytes, stats.activeTransactions);
db.close(); // synchronous, returns void

Transactions

Use transaction() to get an EmbeddedTransaction, or withTransaction() for automatic commit/abort.

commit() returns Promise<void>

EmbeddedTransaction.commit() resolves to Promise<void>, not Promise<bigint>. If you saw older docs claiming commit returns a transaction id or LSN, that was incorrect. (The bigint-returning calls are db.checkpoint() — LSN — and the low-level IpcClient.beginTransaction() — txn id.)

Manual transaction

const db = Database.open('./bank');

const txn = db.transaction(); // synchronous — returns EmbeddedTransaction
try {
await txn.put(Buffer.from('account:1:balance'), Buffer.from('1000'));
await txn.put(Buffer.from('account:2:balance'), Buffer.from('500'));
await txn.commit(); // Promise<void>
} catch (error) {
await txn.abort();
throw error;
}

EmbeddedTransaction supports put, get, delete, putPath, getPath, and scanPrefix(prefix) (an AsyncGenerator<[Buffer, Buffer]>), plus commit() and abort().

const txn = db.transaction();
try {
await txn.put(Buffer.from('key1'), Buffer.from('value1'));
await txn.put(Buffer.from('key2'), Buffer.from('value2'));

for await (const [key, value] of txn.scanPrefix(Buffer.from('key'))) {
console.log(`${key.toString()}: ${value.toString()}`);
}

await txn.commit();
} catch (error) {
await txn.abort();
throw error;
}
Serializable isolation

On commit, SochDB validates serializability. A write-skew / SSI conflict (internal error code -2) causes commit() to throw a TransactionError. Retry the transaction on conflict.

Automatic transactions

withTransaction() commits if the callback returns normally and aborts if it throws.

await db.withTransaction(async (txn) => {
await txn.put(Buffer.from('counter'), Buffer.from('1'));
await txn.put(Buffer.from('timestamp'), Buffer.from(Date.now().toString()));
// commit happens automatically when this resolves
});

Namespaces and collections

Namespaces provide multi-tenant isolation; collections hold vectors backed by a native HNSW index.

These methods are async

On EmbeddedDatabase, the namespace methods (createNamespace, namespace, getOrCreateNamespace, deleteNamespace, listNamespaces) all return promises. Likewise Namespace.createCollection, collection, etc. are async.

Namespaces

import { Database, NamespaceConfig } from '@sochdb/sochdb';

const db = Database.open('./multi_tenant');

const config: NamespaceConfig = {
name: 'tenant_123',
displayName: 'Acme Corp',
labels: { tier: 'enterprise' },
readOnly: false,
};
const ns = await db.createNamespace('tenant_123', config);

// Or fetch an existing one / get-or-create
const existing = await db.namespace('tenant_123');
const ensured = await db.getOrCreateNamespace('tenant_123');

console.log(await db.listNamespaces());

Collections

import { CollectionConfig, DistanceMetric } from '@sochdb/sochdb';

const config: CollectionConfig = {
name: 'documents',
dimension: 384, // default 384
metric: DistanceMetric.Cosine, // Cosine | Euclidean | DotProduct
hnswM: 32, // default 32
hnswEfConstruction: 256, // default 256
};
const collection = await ns.createCollection(config);

DistanceMetric values are Cosine = 'cosine', Euclidean = 'euclidean', DotProduct = 'dot'.

HNSW defaults

The standalone HnswConfig defaults to maxConnections = 32, efConstruction = 256, and efSearch = 100. Collections set a higher efSearch of 500 on their internal native index for stronger recall out of the box; tune it with collection.setEfSearch(n).

Vector operations

insert and insertMany take positional arguments (vector, metadata, id). insertMany uses the native HNSW batch fast path.

// Single insert -> returns the assigned string id
const id = await collection.insert(
[0.1, 0.2, 0.3 /* ...384 dims */],
{ source: 'web', url: 'https://example.com' },
'doc_001' // optional explicit id
);

// Batch insert -> returns string[]
const ids = await collection.insertMany(
[
[0.1 /* ... */],
[0.2 /* ... */],
],
[{ type: 'a' }, { type: 'b' }], // optional metadatas
['doc_1', 'doc_2'] // optional ids
);

search takes a SearchRequest object with a queryVector (not vector):

import { SearchRequest } from '@sochdb/sochdb';

const request: SearchRequest = {
queryVector: queryEmbedding,
k: 10,
filter: { source: 'web' },
includeMetadata: true,
};

const results = await collection.search(request);
for (const r of results) {
// SearchResult: { id, score, vector?, metadata? }
console.log(`ID: ${r.id}, score: ${r.score.toFixed(4)}`);
}

Other collection methods: get(id), delete(id), count(), rebuildIndex(), setEfSearch(n), and the isIndexReady getter.

Keyword/hybrid search lives in the memory module

The Collection API is pure vector search — there is no textQuery/alpha hybrid option on it. For BM25 + vector hybrid retrieval, use HybridRetriever from the memory module.


Standalone HNSW index

For direct, in-process vector indexing without the namespace/collection layer, use HnswIndex.

import { HnswIndex, HnswConfig } from '@sochdb/sochdb';

const config: HnswConfig = {
dimension: 768,
maxConnections: 32, // default 32
efConstruction: 256, // default 256
efSearch: 100, // default 100
};
const index = new HnswIndex(config);

index.insert('doc_1', vector1); // ids are strings (hashed to numeric internally)
index.insertBatch(['doc_2', 'doc_3'], [vector2, vector3]);

const results = index.search(queryVector, 10); // SearchResult[] : { id, distance }
const fast = index.search(queryVector, 10, true); // fast=true for lower-latency search
const ultra = index.searchUltra(queryVector, 10);

console.log(index.length, index.dimension);
index.efSearch = 200; // get/set efSearch at runtime
index.close();
String ids are lossy

String ids are hashed to a numeric id internally; the round-trip back to a string only preserves the low 64 bits. If you need stable original ids, keep your own id mapping.


Priority queue

A durable, O(log N) priority queue with ordered keys, visibility timeouts, and a dead-letter queue.

import { Database, createQueue, PriorityQueue, TaskState } from '@sochdb/sochdb';

const db = Database.open('./queue_db');

const queue = createQueue(db, 'tasks', {
visibilityTimeout: 30000, // ms (default 30000)
maxRetries: 3, // default 3
// deadLetterQueue: 'tasks_dlq',
});
// Equivalent: PriorityQueue.fromDatabase(db, 'tasks', {...})

// Enqueue: (priority, payload, metadata?) -> task id
const taskId = await queue.enqueue(
1, // lower number = higher priority
Buffer.from(JSON.stringify({ action: 'process', orderId: 123 }))
);

// Dequeue and process
const task = await queue.dequeue('worker-1'); // Task | null
if (task) {
try {
// ...do work...
await queue.ack(task.taskId);
} catch (err) {
await queue.nack(task.taskId);
}
}

const stats = await queue.stats(); // QueueStats
const purged = await queue.purge();

TaskState values: PENDING, CLAIMED, COMPLETED, DEAD_LETTERED.


Semantic cache

Cache LLM responses keyed by embedding similarity (cosine).

import { SemanticCache } from '@sochdb/sochdb';

const cache = new SemanticCache(db, 'llm_cache');

const query = 'What is the capital of France?';
const queryEmbedding = await embed(query);

// get(queryEmbedding, threshold = 0.85) -> CacheHit | null
const hit = await cache.get(queryEmbedding, 0.9);
if (hit) {
console.log('Cache hit:', hit.value, 'score', hit.score);
} else {
const response = await callLLM(query);
// put(key, value, embedding, ttlSeconds = 0, metadata?)
await cache.put(query, response, queryEmbedding, 3600, { query });
}

const stats = await cache.stats(); // { count, hits, misses, hitRate, memoryUsage }
const expired = await cache.purgeExpired();

Context builder

Token-aware, priority-based assembly of LLM context.

import { createContextBuilder, ContextOutputFormat, TruncationStrategy } from '@sochdb/sochdb';

const result = await createContextBuilder()
.forSession('session_42')
.withBudget(4096) // tokens (default 4096)
.setFormat(ContextOutputFormat.TOON)
.setTruncation(TruncationStrategy.TAIL_DROP)
.literal('system', 100, 'You are a helpful assistant.')
.section('history', 50)
.last(10, 'messages')
.done()
.section('knowledge', 40)
.search('documents', queryEmbedding, 5)
.done()
.execute();

console.log(`Tokens used: ${result.tokenCount}`);
console.log(result.text);

ContextOutputFormat values: TOON, JSON, MARKDOWN. TruncationStrategy values: TAIL_DROP, HEAD_DROP, PROPORTIONAL. execute() returns ContextResult { text, tokenCount, sections[] }.


Memory system

Structured memory: LLM-driven extraction, consolidation into canonical facts, and hybrid (BM25 + vector) retrieval.

Extraction

import { ExtractionPipeline } from '@sochdb/sochdb';

// new ExtractionPipeline(db, namespace, schema?)
const pipeline = new ExtractionPipeline(db, 'memory', {
entities: ['Person', 'Organization', 'Product'],
relations: ['works_at', 'owns', 'purchased'],
});

// You supply the extractor function (your LLM call)
const result = await pipeline.extract(
'Alice works at Acme Corp and purchased a new laptop.',
async (text) => callMyLLMExtractor(text)
);

console.log(result.entities, result.relations);
await pipeline.commit(result);
// or: await pipeline.extractAndCommit(text, extractor)

Consolidation

Merge raw assertions into canonical facts, with contradiction handling.

import { Consolidator } from '@sochdb/sochdb';

const consolidator = new Consolidator(db, 'memory');

const assertionId = await consolidator.add(rawAssertion);
await consolidator.addWithContradiction(newAssertion, [assertionId]);

const merged = await consolidator.consolidate(); // -> number of canonical facts
const facts = await consolidator.getCanonicalFacts();
const explanation = await consolidator.explain(facts[0].id);

Hybrid retrieval

import { HybridRetriever, AllowedSet } from '@sochdb/sochdb';

// new HybridRetriever(db, namespace, collection, config?)
const retriever = new HybridRetriever(db, 'memory', 'documents', {
k: 10,
alpha: 0.5, // blend of vector vs BM25
enableRerank: false,
rerankK: 100,
});

await retriever.indexDocuments(docs);

// Pre-filter results with an AllowedSet
const allowed = AllowedSet.fromNamespace('memory');

const response = await retriever.retrieve(
'What did Alice buy?',
queryEmbedding,
allowed,
10
);

for (const item of response.results) {
console.log(`${item.content} (score: ${item.score.toFixed(3)})`);
}

AllowedSet factories: AllowedSet.fromIds(ids), .fromNamespace(ns), .fromFilter(fn), .allowAll().


MCP integration

Expose your database to LLM agents over the Model Context Protocol, or consume an MCP server.

MCP tool naming

SochDB's first-party MCP tool names use underscores (for example sochdb_query, sochdb_get, sochdb_put, sochdb_context_query, memory_search_episodes, sochdb_grep). Any dot-separated names you may find in older catalogs are stale.

MCP server

import { McpServer } from '@sochdb/sochdb';

// new McpServer(db, config)
const server = new McpServer(db, {
name: 'sochdb-mcp',
version: '1.0.0',
});

// registerTool(tool, handler) — the handler is a SEPARATE second argument
server.registerTool(
{
name: 'search_documents',
description: 'Search documents by semantic similarity',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string' },
k: { type: 'number', default: 10 },
},
},
},
async (input) => {
const results = await collection.search({ queryVector: await embed(input.query), k: input.k });
return { results };
}
);

console.log(server.listTools());
const result = await server.callTool({ name: 'search_documents', arguments: { query: 'ml', k: 5 } });

McpServer also supports unregisterTool, registerPrompt/listPrompts/getPrompt, listResources/readResource, and getServerInfo.

MCP client

import { McpClient } from '@sochdb/sochdb';

const client = new McpClient({ /* McpClientConfig: transport, etc. */ });
await client.connect();

const tools = await client.listTools();
const result = await client.callTool('search_documents', { query: 'machine learning', k: 5 });

await client.disconnect();

Errors surface as McpError(message, code, data?); standard codes are in MCP_ERROR_CODES.


Policy service

Namespace-level ACL and governance.

import { PolicyService } from '@sochdb/sochdb';

// new PolicyService(db, { enableAudit? })
const policy = new PolicyService(db, { enableAudit: true });

await policy.createNamespacePolicy(namespacePolicy);
await policy.addRule('tenant_123', policyRule);

const evaluation = await policy.evaluate(policyRequest); // PolicyEvaluation
const granted = await policy.grantAccess(namespaceGrant);
const ok = await policy.hasPermission(/* ... */);

const audit = await policy.getAuditLog();

Other methods include getNamespacePolicy, updateNamespacePolicy, deleteNamespacePolicy, removeRule, revokeAccess, listGrants, and clearCache.


Graph overlay

Thin KV-backed graph helpers on EmbeddedDatabase. Nodes and edges are stored under _graph/{namespace}/... keys.

await db.addNode('social', 'alice', 'Person', { name: 'Alice' });
await db.addNode('social', 'bob', 'Person', { name: 'Bob' });
await db.addEdge('social', 'alice', 'follows', 'bob', { since: 2026 });

// traverse(namespace, startNode, maxDepth = 10, order = 'bfs')
const { nodes, edges } = await db.traverse('social', 'alice', 3, 'bfs');
console.log(nodes.length, edges.length);

Server mode (gRPC / IPC)

gRPC client

import { SochDBClient } from '@sochdb/sochdb';

const client = new SochDBClient({ address: 'localhost:50051' });

// Vector index
await client.createIndex('idx', 768, { metric: 'cosine', m: 32, efConstruction: 256 });
await client.insertVectors('idx', [1, 2, 3], [v1, v2, v3]);
const hits = await client.search('idx', queryVector, 10, 50); // (index, query, k=10, ef=50)

// Collections
await client.createCollection('docs', { dimension: 768, namespace: 'default', metric: 'cosine' });
await client.addDocuments('docs', [{ id: 'd1', content: 'hello', embedding: v1 }]);
const docs = await client.searchCollection('docs', queryVector, 10);

// KV — note key/value come first, namespace is optional
await client.put(Buffer.from('key'), Buffer.from('value')); // put(key, value, namespace?, ttlSeconds?)
const value = await client.get(Buffer.from('key')); // get(key, namespace?)
await client.delete(Buffer.from('key')); // delete(key, namespace?)

client.close();

createIndex defaults m = 32 and efConstruction = 256, matching the engine's HnswConfig::default(). The client also exposes graph (addNode/addEdge/traverse), cache (cacheGet/cachePut), and tracing (startTrace/startSpan/endSpan) calls. SearchResult from gRPC is { id: number, distance: number }.

IPC client

Connects to a local server over a Unix domain socket. Not available on Windows.

import { IpcClient } from '@sochdb/sochdb';

const client = await IpcClient.connect('/tmp/sochdb.sock');

await client.put(Buffer.from('key'), Buffer.from('value'));
const value = await client.get(Buffer.from('key'));

const txnId = await client.beginTransaction(); // Promise<bigint> (txn id)
await client.commitTransaction(txnId);

const entries = await client.scan('users/'); // Array<{ key, value }>
const alive = await client.ping(); // Promise<boolean>
client.close();

Error handling

All SochDB errors extend SochDBError, which carries a .code (from the ErrorCode enum) and an optional .remediation string.

import { SochDBError, ErrorCode, TransactionError } from '@sochdb/sochdb';

try {
await db.withTransaction(async (txn) => {
await txn.put(Buffer.from('k'), Buffer.from('v'));
});
} catch (err) {
if (err instanceof TransactionError) {
// Retry on serialization conflict
} else if (err instanceof SochDBError) {
console.error(`SochDB error ${err.code}: ${err.message}`, err.remediation);
}
}

Error codes

CodeNameCodeName
1001CONNECTION_FAILED9001INTERNAL_ERROR
1002CONNECTION_TIMEOUT9003STORAGE_ERROR
1003CONNECTION_CLOSED10001DATABASE_LOCKED
1004PROTOCOL_ERROR10002LOCK_TIMEOUT
2001TRANSACTION_ABORTED10003EPOCH_MISMATCH
2002TRANSACTION_CONFLICT10004SPLIT_BRAIN
10005STALE_LOCK

Error classes

  • Base: SochDBError(message, code = INTERNAL_ERROR, remediation?)
  • Connection / protocol / storage: ConnectionError, ProtocolError, DatabaseError
  • Transactions: TransactionError
  • Locking: LockError, DatabaseLockedError(path, holderPid?) (with .path, .holderPid), LockTimeoutError(path, timeoutSecs), EpochMismatchError(expected, actual), SplitBrainError(message?)
  • Namespace / collection: NamespaceNotFoundError, NamespaceExistsError, CollectionNotFoundError, CollectionExistsError
  • Modules: StudioAPIError (Studio HTTP client), McpError

Studio client

HTTP client for the hosted SochDB Studio backend (Bearer auth).

import { StudioClient } from '@sochdb/sochdb';

const studio = new StudioClient({ baseUrl: 'https://studio.example.com', apiKey: 'sk_...' });

const health = await studio.health();
const result = await studio.ingestEvents(events, { source: 'my-app' });
console.log(result.ingested, result.eventIds);

Errors surface as StudioAPIError(message, statusCode?).


TypeScript usage

The package ships full type definitions.

import { Database } from '@sochdb/sochdb';

interface User {
name: string;
email: string;
age: number;
}

async function putJSON<T>(db: Database, key: string, value: T): Promise<void> {
await db.put(Buffer.from(key), Buffer.from(JSON.stringify(value)));
}

async function getJSON<T>(db: Database, key: string): Promise<T | null> {
const raw = await db.get(Buffer.from(key));
return raw ? (JSON.parse(raw.toString()) as T) : null;
}

const db = Database.open('./my_db');
await putJSON<User>(db, 'users/alice', { name: 'Alice', email: 'alice@example.com', age: 30 });
const user = await getJSON<User>(db, 'users/alice');

Best practices

1. Always close the database

const db = Database.open('./my_db');
try {
await db.put(Buffer.from('key'), Buffer.from('value'));
} finally {
db.close(); // synchronous
}

2. Use scanPrefix for prefix queries

const results: Array<[Buffer, Buffer]> = [];
for await (const [key, value] of db.scanPrefix(Buffer.from('users/'))) {
results.push([key, value]);
}

3. Use transactions for atomicity

await db.withTransaction(async (txn) => {
await txn.put(Buffer.from('counter'), Buffer.from('1'));
await txn.put(Buffer.from('timestamp'), Buffer.from(Date.now().toString()));
});

4. Handle missing keys explicitly

const value = await db.get(Buffer.from('key'));
if (value === null) {
console.log('Key not found');
} else {
console.log('Value:', value.toString());
}

5. Keys and values must be Buffers

// Correct — binary-safe Buffers
await db.put(Buffer.from('key'), Buffer.from([0x00, 0x01, 0x02]));

Complete example: multi-tenant store

import { Database } from '@sochdb/sochdb';

interface TenantUser {
id: string;
role: string;
email: string;
}

async function main() {
const db = Database.open('./saas_db');

try {
await db.put(
Buffer.from('tenants/acme/users/alice'),
Buffer.from(JSON.stringify({ id: 'alice', role: 'admin', email: 'alice@acme.com' }))
);
await db.put(
Buffer.from('tenants/acme/users/bob'),
Buffer.from(JSON.stringify({ id: 'bob', role: 'user', email: 'bob@acme.com' }))
);
await db.put(
Buffer.from('tenants/globex/users/charlie'),
Buffer.from(JSON.stringify({ id: 'charlie', role: 'admin', email: 'charlie@globex.com' }))
);

for (const tenant of ['acme', 'globex']) {
const users: TenantUser[] = [];
for await (const [, value] of db.scanPrefix(Buffer.from(`tenants/${tenant}/users/`))) {
users.push(JSON.parse(value.toString()));
}
console.log(`\n${tenant} (${users.length} users):`);
for (const u of users) console.log(` ${u.email} (${u.role})`);
}
} finally {
db.close();
}
}

main().catch(console.error);

API reference (embedded)

EmbeddedDatabase (exported as Database)

MethodReturnsNotes
Database.open(path, config?)EmbeddedDatabasesynchronous
Database.openConcurrent(path, options?)EmbeddedDatabasemulti-process MVCC
Database.isConcurrentModeAvailable()booleanstatic
put(key, value)Promise<void>Buffer key/value
get(key)Promise<Buffer | null>
delete(key)Promise<void>
putPath(path, value) / getPath(path)Promise<void> / Promise<Buffer | null>
scanPrefix(prefix)AsyncGenerator<[Buffer, Buffer]>single prefix
transaction()EmbeddedTransactionsynchronous
withTransaction(fn)Promise<T>auto commit/abort
checkpoint()Promise<bigint>returns LSN
stats()Promise<{...}>
close()voidsynchronous

EmbeddedTransaction

MethodReturns
put / get / deletePromise<void> / Promise<Buffer | null> / Promise<void>
putPath / getPathPromise<void> / Promise<Buffer | null>
scanPrefix(prefix)AsyncGenerator<[Buffer, Buffer]>
commit()Promise<void>
abort()Promise<void>

Resources


Last updated: June 2026 (Node.js SDK v0.5.3)