Node.js
TalaDB's Node.js integration uses a prebuilt native .node module via napi-rs. The same Rust engine powers document queries and vector search natively — no WASM, no subprocess — with performance identical to embedding the library directly in Rust.
Requirements
- Node.js 18+
- Supported platforms:
linux-x64,linux-arm64,darwin-x64,darwin-arm64(Apple Silicon),win32-x64
Installation
pnpm add taladb @taladb/nodeQuick start
import { openDB } from 'taladb'
const db = await openDB('./myapp.db')
const users = db.collection('users')
await users.insert({ name: 'Alice', age: 30 })
const all = await users.find()openDB detects Node.js automatically and routes calls through the native module. The database file is created if it does not exist. Parent directories must exist.
Change webhook
Report every committed write to a backend over HTTP. Add a webhook block to taladb.config.yml next to the engine's durability settings:
webhook:
enabled: true
endpoint: "https://api.example.com/taladb"
headers:
Authorization: "Bearer my-token"
exclude_fields:
- embeddingopenDB auto-discovers the file — no code change needed. Or pass it inline:
const db = await openDB('app.db', {
webhook: { enabled: true, endpoint: 'https://api.example.com/taladb' },
})See the Change Webhook reference for payload shapes, retry behaviour, and the best-effort delivery/idempotency contract.
Basic CRUD
import { openDB } from 'taladb'
interface Task {
_id?: string
title: string
done: boolean
priority: 1 | 2 | 3
createdAt: number
}
const db = await openDB('./tasks.db')
const tasks = db.collection<Task>('tasks')
// Indexes — create once at startup, idempotent
await tasks.createIndex('done')
await tasks.createIndex('priority')
// Insert — returns the generated ULID string
const id = await tasks.insert({
title: 'Write documentation',
done: false,
priority: 1,
createdAt: Date.now(),
})
// Find
const urgent = await tasks.find({ $and: [{ done: false }, { priority: 1 }] })
const task = await tasks.findOne({ _id: id })
// Update
await tasks.updateOne({ _id: id }, { $set: { done: true } })
// Delete
const removed = await tasks.deleteMany({ done: true })Full-text search — BM25 ranking
Create an FTS index on any string field, then rank by relevance with searchText (BM25) or filter booleanly with $contains:
const posts = db.collection<Post>('posts')
// Create once at startup — idempotent
await posts.createFtsIndex('body')
// Ranked search — best matches first (OR semantics)
const hits = await posts.searchText('body', 'embedded rust database', 5)
// [{ document: Post, score: 3.14 }, ...]
// Boolean filter — every token must be present
const all = await posts.find({ body: { $contains: 'taladb' } })Hybrid search — keyword + vector
Fuse BM25 and vector rankings with reciprocal rank fusion — the on-device RAG retrieval recipe. Requires an FTS index and a vector index on the collection:
const results = await posts.hybridSearch(
{ textField: 'body', text: 'how do I get a refund' },
{ vectorField: 'embedding', vector: await embed('how do I get a refund') },
5,
)
// [{ document, score, textRank, vectorRank }, ...]See the Search reference for filters and tuning.
Inspecting indexes
const { btree, fts, vector } = await tasks.listIndexes()
// btree: ['done', 'priority']
// fts: []
// vector: []Live queries
subscribe fires the callback immediately and again after any write that affects the result:
const unsub = tasks.subscribe({ done: false }, (pending) => {
console.log(`${pending.length} tasks remaining`)
})
// Later — stop listening
unsub()Vector search
TalaDB's vector index works on Node.js with any embedding source — a local model, the OpenAI API, or anything that returns a number[].
// Option A: local model, no API key
import { pipeline } from '@huggingface/transformers'
const embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2')
async function embed(text: string): Promise<number[]> {
const out = await embedder(text, { pooling: 'mean', normalize: true })
return Array.from(out.data) as number[]
}
// Option B: OpenAI
import OpenAI from 'openai'
const openai = new OpenAI()
async function embed(text: string): Promise<number[]> {
const res = await openai.embeddings.create({ model: 'text-embedding-3-small', input: text })
return res.data[0].embedding
}interface Doc {
_id?: string
content: string
source: string
embedding: number[]
}
const docs = db.collection<Doc>('docs')
await docs.createVectorIndex('embedding', { dimensions: 384 })
// Insert
await docs.insert({ content, source: 'readme', embedding: await embed(content) })
// Semantic search
const results = await docs.findNearest('embedding', await embed('local database'), 5)
for (const { document, score } of results) {
console.log(score.toFixed(3), document.content)
}
// Filtered vector search: filter + rank in one call
const filtered = await docs.findNearest('embedding', await embed('local database'), 5, {
source: 'readme',
})HNSW for large collections
On Node.js, HNSW (approximate nearest-neighbour) is available for faster search on large collections:
// Create with HNSW from the start
await docs.createVectorIndex('embedding', {
dimensions: 384,
indexType: 'hnsw',
hnswM: 32, // only supported connectivity value
hnswEfConstruction: 200, // build-time quality
})
// Or upgrade an existing flat index in-place (no data loss)
await docs.upgradeVectorIndex('embedding')
// findNearest API is identical — HNSW is transparent
const results = await docs.findNearest('embedding', queryVec, 10)Ingestion script
import { openDB } from 'taladb'
import fs from 'node:fs/promises'
const db = await openDB('./knowledge.db')
const docs = db.collection<Doc>('docs')
await docs.createVectorIndex('embedding', { dimensions: 1536 })
const files = await fs.readdir('./content')
for (const file of files) {
const content = await fs.readFile(`./content/${file}`, 'utf8')
await docs.insert({ content, source: file, embedding: await embed(content) })
}
console.log(`Indexed ${files.length} documents`)
await db.close()Server example
// server.ts — Express + TalaDB
import express from 'express'
import { openDB } from 'taladb'
interface Event {
_id?: string
type: string
payload: Record<string, unknown>
ts: number
}
const app = express().use(express.json())
const db = await openDB('./events.db')
const events = db.collection<Event>('events')
await events.createIndex('type')
await events.createIndex('ts')
app.post('/events', async (req, res) => {
const id = await events.insert({ type: req.body.type, payload: req.body.payload ?? {}, ts: Date.now() })
res.json({ id })
})
app.get('/events', async (req, res) => {
const filter: Record<string, unknown> = {}
if (req.query.type) filter.type = req.query.type
if (req.query.since) filter.ts = { $gte: Number(req.query.since) }
res.json(await events.find(filter))
})
app.listen(3000)Migrations
Each migration runs once at open time, in version order, inside an atomic transaction:
const db = await openDB('./myapp.db', {
migrations: [
{
version: 1,
description: 'Index users by email',
up: async (db) => {
await db.collection('users').createIndex('email')
},
},
],
})Backups
The @taladb/node binding does not expose snapshot export — that lives in the Rust core and is surfaced only by @taladb/web. On Node, the database is a single file, so a backup is a file copy:
import fs from 'node:fs/promises'
await db.close() // release the exclusive lock
await fs.copyFile('./myapp.db', './backup.db')Copy it while the database is closed, or from a filesystem snapshot. Copying an open database can capture a torn write.
Testing with an in-memory database
// vitest / jest
import { TalaDBNode } from '@taladb/node'
let db: ReturnType<typeof TalaDBNode.openInMemory>
beforeEach(() => { db = TalaDBNode.openInMemory() })
afterEach(() => { db.close() })No file system cleanup, no interference between test runs.
Closing
await db.close()Always close the database before the process exits to flush any pending writes and release the file lock.