vantadb
v0.9.0
Published
VantaDB TypeScript SDK — browser AI agent memory: WASM-powered embedded vector & graph memory for JS runtimes (Node, Bun, Deno, browser)
Maintainers
Readme
VantaDB TypeScript SDK
WASM-powered embedded vector & graph memory for JavaScript runtimes — browser AI agent memory.
Status: active (core-promise until 1.0 — published as
vantadbon npm).Try it in the browser: AI agent demo — Transformers.js embeddings on-device + OPFS memory (
connect_persistent()from the rawvantadb-wasmbinding), no server.
import { Client } from "vantadb";
const db = Client.create();
await db.put({ namespace: "docs", key: "intro", payload: "VantaDB is a vector database", vector: [0.1, 0.2, ...] });
const results = await db.search({ namespace: "docs", query_vector: [0.15, 0.25, ...], top_k: 5 });Features
- Vector search — cosine/euclidean HNSW search
- Hybrid search — vector + BM25 text fusion (RRF)
- Graph queries — BFS, DFS, topological sort
- Persistence — export/import JSONL
- TTL — auto-expiring records
- Works everywhere — Node.js, Bun, Deno, browsers
Install
npm install vantadbQuick Start
import { Client } from "vantadb";
// In-memory (default)
const db = Client.create();
// Or persistent:
// const db = Client.open("./vanta_data");
// Store
await db.put({
namespace: "memories",
key: "greeting",
payload: "Hello, world!",
metadata: { lang: "en" },
vector: [0.1, 0.2, 0.3],
});
// Search
const hits = await db.search({
namespace: "memories",
query_vector: [0.1, 0.2, 0.3],
top_k: 10,
});
console.log(hits[0].record.payload); // "Hello, world!"
db.close();Module Formats (ESM / CommonJS)
The package is ESM-only (the vantadb-wasm dependency is an ES module and the
API is fully synchronous, so a CommonJS build would not be loadable without an
async rewrite). import works everywhere — Node, Bun, Deno, bundlers.
CommonJS consumers on Node.js ≥ 22.12 can require("vantadb") directly:
Node's require(esm) loads the module graph because it contains no top-level
await. On older Node versions, migrate the consumer to import, or use the
native backend (vantadb-node) whose API is async and
CommonJS-friendly.
// Node.js >= 22.12
const { Client } = require("vantadb");
const db = Client.create();
// ...same synchronous API as the ESM buildWASM bundle & lazy loading
The package is backed by the vantadb-wasm wasm-bindgen build; the compiled
engine binary is vantadb-wasm/pkg/vantadb_wasm_bg.wasm (~1.8 MB raw,
~739 KB gzipped transfer — measured 2026-10-04, see the
bundle strategy).
How it is loaded depends on the runtime:
- Bundlers (Vite/Webpack/esbuild) — the wasm-bindgen glue
(
vantadb_wasm.js) imports the.wasmas an ES module, which bundlers cannot handle natively. Addvite-plugin-wasm(or the equivalent for your bundler) so the binary is fetched + instantiated on demand. Without a plugin the build fails at bundle time. - Node.js — the wasm-bindgen Node loader reads the
.wasmfile from disk at first use; no plugin required. - Vanta Studio desktop/web builds — the WASM backend is code-split out:
the glue module is externalized so the lazy
import()never executes in Tauri/HTTP modes (seefeature-freez/desktop/vite.config.ts, WASM-02). It only loads invite build --mode wasm(WASM-03).
Zero-install CDN usage (verified 2026-08-26)
| CDN entry | Works? | Why |
|-----------|--------|-----|
| https://cdn.jsdelivr.net/npm/vantadb@latest/+esm | ❌ No | The SDK bundle itself is fine, but it imports /npm/[email protected]/+esm, and jsDelivr's Rollup/esbuild pipeline fails to bundle the wasm-bindgen glue: the generated vantadb_wasm.js contains import * as wasm from "./vantadb_wasm_bg.wasm" (wasm-pack bundler target — a binary imported as an ES module). Rollup cannot resolve it and jsDelivr serves a stub module that throws on import: "Failed to bundle using Rollup: failed to resolve an internal import". |
| https://esm.sh/vantadb@latest | ✅ Yes | esm.sh's build inlines the .wasm binary as a base64 byte array into the served .mjs (no sidecar fetch needed), so the bundler-target glue initializes directly in the browser. |
<script type="module">
// Verified working (wasm is inlined by esm.sh's build):
import { Client } from "https://esm.sh/vantadb";
const db = Client.create();
await db.put({ namespace: "demo", key: "k", payload: "hello" });
console.log((await db.get({ namespace: "demo", key: "k" }))?.payload); // "hello"
db.close();
</script>If you prefer self-hosting over third-party CDN transforms, rebuild the
binding for browsers with wasm-pack build --target web (the web target uses
fetch() + WebAssembly.instantiateStreaming instead of the ES-module import)
and serve the output files yourself.
SSR / React hooks guidance:
- Do not instantiate the engine during server rendering — the wasm loader
needs browser APIs (
fetch/WebAssembly) or the file system, neither of which is guaranteed in a server context. - Create the client lazily in client-only code:
Client.create()inside auseEffect/useMemo(or a framework's client boundary), never at module top-level of an SSR-shared file. - On Node.js, prefer the native backend (
NativeVantaDB) — it is loaded lazily via dynamicimport()and gives real filesystem persistence (fjall/WAL) that the WASM build cannot.
Console logging
The WASM engine installs a console.log-based tracing subscriber at level
WARN — DEBUG traces (e.g. the core's env-var config dump) are not
printed by default. To opt into more detail, set the global before the
first client is created:
globalThis.VANTADB_LOG = "debug"; // "trace" | "debug" | "info" | "warn" | "error"
const db = Client.create();The value is read once per process (the subscriber is global); an absent or
invalid value falls back to WARN.
Bundle size vs JavaScript-only competitors
Measured 2026-10-04 (VantaDB row; competitor rows as dated below). Reproducible: see
../vantadb-wasm/README.md §1.
| Library | Version | Gzipped | Vector | Hybrid | Persistence | |---------|---------|--------:|--------|--------|-------------| | @orama/orama | 3.1.18 | 23.8 KB | ✅ | ✅ RRF | ❌ (in-mem + plugin) | | MiniSearch | latest | 5.9 KB | ❌ | ❌ | ❌ | | Lunr | 2.3.9 | 8.1 KB | ❌ | ❌ | ❌ | | vantadb WASM | 0.8.x | ~739 KB transfer (1.77 MB raw wasm; measured 2026-10-04) | ✅ HNSW | ✅ BM25 + RRF | ✅ OPFS / IDB / in-mem |
VantaDB is ~31× larger than Orama gzipped, but ships OPFS persistence,
HNSW (p99 441 µs at 100K), TTL auto-expiry, capability graph — features none of
the JS-only engines include. Honest tradeoff: choose Orama (23.8 KB) if you
only need full-text + RAG in-memory with no persistence; choose VantaDB if
any of those features matter. Full feature-gap analysis:
../vantadb-wasm/README.md §4.
vantadb vs vantadb-node (npm)
Two npm packages exist; they are not the same thing:
| Package | What it is | Published | API |
|---------|------------|-----------|-----|
| vantadb | TypeScript SDK over the WASM build — works in browsers, Node, Bun, Deno | ✅ 0.8.0 | Synchronous, ESM-only |
| vantadb-node | Native Node.js bindings (napi-rs) — real filesystem persistence (fjall/WAL/fsync), async API, platform-specific .node binaries | ❌ not yet published (registry 404) | Async, ESM + CommonJS |
vantadb-node is the native backend you reach via
NativeVantaDB.connect() (lazy-loaded); vantadb is the WASM backend
(Client.create()). See ADR-0030 (docs/dev/architecture/adr/ADR-0030-brand-identity-naming-convention.md)
for the full naming convention across registries.
Errors
All operations throw VantaError, an Error subclass with a
machine-readable code. The WASM binding attaches the code structurally (no
message parsing required); older vantadb-wasm builds fall back to message
classification.
| code | Meaning |
|--------|---------|
| NOT_FOUND | Node / namespace / record not found |
| VALIDATION_ERROR | Invalid input, dimension mismatch, duplicate node, zero-norm cosine query, empty namespace… |
| CORRUPT | Incompatible binary format, WAL version mismatch, serialization/schema error |
| RESOURCE_LIMIT | Memory or configured resource limit exceeded |
| TIMEOUT | Operation exceeded its time budget |
| BUSY | Database busy (lock held) |
| IO_ERROR | WAL / IO / backend storage error |
| CLOSED | Operation on a closed instance |
| WASM_ERROR | Unclassified engine error (catch-all) |
try {
db.search({ namespace: "docs", query_vector: [0, 0, 0] });
} catch (err) {
if (err instanceof VantaError) {
console.log(err.code); // "VALIDATION_ERROR"
}
}Real Embeddings
The examples above use toy vectors. Generate real ones with your own client —
local Ollama or the OpenAI API — and pass them to
put() / search():
const res = await fetch("http://localhost:11434/api/embed", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: "nomic-embed-text", input: "Hello, world!" }),
});
const [vec] = (await res.json()).embeddings;
await db.put({ namespace: "memories", key: "greeting", payload: "Hello, world!", vector: vec });
const hits = await db.search({ namespace: "memories", query_vector: vec, top_k: 10 });
console.log(hits[0].record.payload); // "Hello, world!"Use one embedding model per namespace — stored and query vectors must share the same dimensionality. Full walkthrough: QUICKSTART → Real Embeddings.
API
Lifecycle
| Method | Description |
|--------|-------------|
| VantaDB.create(config?) | Create in-memory or configured instance |
| Client.open(path) | Open persistent store from disk |
| .close() | Free WASM resources |
CRUD
| Method | Description |
|--------|-------------|
| .put(input) | Store a memory record |
| .putBatch(inputs) | Batch store |
| .get({namespace, key}) | Retrieve by key |
| .delete({namespace, key}) | Remove by key |
| .deleteByFilter({namespace, filter}) | Batch delete matching an AND-combined filter; returns count (rejects empty filter) |
| .list({namespace, ...options}) | List with pagination |
| .listNamespaces() | List all namespaces |
Search
| Method | Description |
|--------|-------------|
| .search(request) | Hybrid vector + text search |
| .searchVector({vector, topK}) | Pure vector search |
| .explainSearch(request) | Search with score breakdown |
Graph
| Method | Description |
|--------|-------------|
| .insertNode(id, content?, vector?, fields?) | Create a graph node |
| .getNode(id) | Get node with edges |
| .deleteNode(id, reason?) | Remove node |
| .addEdge(source, target, label?, weight?) | Create edge |
| .graphBfs(roots, maxDepth?) | BFS traversal |
| .graphDfs(roots, maxDepth?) | DFS traversal |
| .graphTopologicalSort(roots) | Topological sort |
| .graphIsDag(roots) | Check if DAG |
Maintenance
| Method | Description | WASM caveat |
|--------|-------------|-------------|
| .flush() | Flush WAL to storage | Engine buffers only — not a durability guarantee; browser persistence is via OPFS/IDB (WASM bundle & lazy loading), real WAL/fsync on the native backend |
| .compactWal() | Compact WAL | WASM: engine-internal WAL — same durability caveat as .flush() |
| .purgeExpired() | Remove TTL-expired records | — |
| .rebuildIndex() | Rebuild ANN index | — |
| .compactLayout() | Compact storage layout | — |
| .operationalMetrics() | Get runtime metrics | — |
| .capabilities() | Get build capabilities | — |
Export / Import
| Method | Description | WASM caveat |
|--------|-------------|-------------|
| .exportNamespace(path, namespace) | Export a namespace to JSONL | Not supported on the WASM runtime — throws IO error: operation not supported on this platform (no std::fs) |
| .exportAll(path) | Export all namespaces to JSONL | Not supported on the WASM runtime — same platform IO error |
| .importRecords(records) | Import records from an array | — (no filesystem involved) |
| .importFile(path) | Import records from a JSONL file | Not supported on the WASM runtime — same platform IO error; use .importRecords() for array input |
FS-backed export/import needs a filesystem-backed runtime, which the WASM build cannot reach (tracked as FIND-79; a native/WASI wiring is deferred). For file JSONL round-trips today use
vanta-cli export/vanta-cli importor the Python SDK (PYTHON_SDK.md);.importRecords()covers in-memory import. Backend split: vantadb vs vantadb-node.
Text Index
| Method | Description |
|--------|-------------|
| .auditTextIndex(namespace?) | Audit text index integrity |
| .auditTextIndexDeep(namespace?) | Deep structural text index audit |
| .repairTextIndex() | Repair text index from canonical storage |
Utilities
| Method | Description |
|--------|-------------|
| .query(iqlQuery) | Execute IQL query |
| .generateSnippet(payload, query, withHighlighting?) | Generate highlighted text snippet |
Cross-SDK Search Parity
VantaDB exposes the same search capabilities across bindings, with aligned
names in both SDKs. The canonical method→domain map lives in
docs/api/BINDINGS_NAMESPACES.md.
| Capability | TypeScript SDK | Python SDK |
|---|---|---|
| search() meaning | Hybrid search (vector + text) → returns SearchHit[] | Hybrid memory search (vector + text) → returns SearchHit[] |
| Hybrid (vector + text) | search({ namespace, query_vector, text_query }) | search(namespace, query_vector, text_query=...) |
| Pure vector ANN | searchVector(vector, topK?) | search_vector(vector, top_k=10) |
| Namespace scoping | search({ namespace }) | search(namespace, ...) |
| Filters | search({ filters }) | search(namespace, query_vector, filters=...) |
| top_k | search({ top_k }) / searchVector(v, topK) | search(..., top_k=) / search_vector(..., top_k=) |
| distance_metric | search({ distance_metric: "Cosine"/"Euclidean" }) | search(..., distance_metric="cosine"/"euclidean") |
| text_query | search({ text_query }) | search(..., text_query=...) |
| Explain | search({ explain }) + explainSearch() | search(..., explain=True) + explain_memory_search() |
| Batch search | — | search_batch(vectors) / search_batch_requests(requests) — Python-only |
| Hybrid method / profile override | — | search(..., method=...) — Python-only |
Porting note: both SDKs use
search()for hybrid retrieval and a distinctsearchVector()/search_vector()for pure vector ANN (AST-008 aligned).method=(dense-index override) is Python-only.
Domain Sub-clients
Every flat method is also reachable through a domain sub-client: db.memory.*, db.graph.*, db.wiki.*, db.system.*. Sub-clients are pure organizational sugar over the flat API — each call forwards verbatim to the flat method of the same behavior.
Backward-compat guarantee: the flat API is unchanged.
db.memory.put(x)anddb.put(x)are the same call; existing code keeps working as-is. Canonical method→domain map:docs/api/BINDINGS_NAMESPACES.md.
// memory — namespace+key records, search, TTL
await db.memory.put({ namespace: "docs", key: "intro", payload: "...", vector: [0.1, 0.2] });
const hits = await db.memory.search({ namespace: "docs", query_vector: [0.15, 0.25], top_k: 5 });
await db.memory.purgeExpired();
// graph — node/edge CRUD + traversals (traversals use short names)
const node = await db.graph.getNode(42);
const reachable = await db.graph.bfs([42], 3);
const order = await db.graph.topologicalSort([1, 2, 3]);
if (await db.graph.isDag([1, 2])) { /* safe to topologically sort */ }
// wiki — node archive recovery (summary/archive lifecycle)
const recovered = await db.wiki.recoverArchivedNodes(42);
// system — lifecycle, metrics, IQL, maintenance, import/export
console.log(await db.system.capabilities());
const result = await db.system.query("(match (node :content \"rust\") (return node))");
await db.system.flush();Notes:
- Sub-clients are lazy, frozen (
Readonly) singletons — accessingdb.graphtwice returns the same object. conversation/skillssub-clients do not exist yet; their capabilities live in the core crate only. Wiki slice 1 (recoverArchivedNodes) landed in TS-11; the remaining wiki / conversation / skills surfaces are tracked with exact dependencies and promotion criteria in the Sub-Client Roadmap.- Python exposes the equivalent grouping via
db.memory,db.graph,db.system,db.wiki— see PYTHON_SDK.md → Domain Sub-clients.
Runtimes
| Runtime | Status |
|---------|--------|
| Node.js 22.12+ | ✅ (ESM + require() via require(esm)) |
| Node.js 18–22.11 | ⚠️ ESM only — use import |
| Bun | ✅ |
| Deno | ✅ |
| Browser | ✅ (ESM) |
Examples
See the examples/ directory:
- Vercel AI SDK — streaming chat with VantaDB memory
- LangChain — LangChain vector store integration
- LlamaIndex — LlamaIndex document store
License
Apache 2.0
