@bod.ee/db
v2.3.3
Published
Readme
BodDB
228 tests passing · ~200k ops/sec · Zero dependencies · Full docs + examples
Embedded reactive database — SQLite-backed, zero dependencies, Firebase-like API.
- In-process: no cloud, no external services
- SQLite WAL: crash-safe, instant startup
- Real-time: value + child subscriptions with ancestor propagation
- Network: WebSocket + REST + SSE transport with auth, permission rules, and rate limiting
- Transforms: increment, serverTimestamp, arrayUnion, arrayRemove, refs
- Streaming: Kafka-like consumer groups with offset tracking, replay, compaction
- Message Queue: SQS-style work queue — exactly-once delivery, visibility timeout, DLQ
- Advanced: transactions, batch ops, push IDs, TTL, FTS5, vector search, file sync
- TypeScript-first: full type safety, Bun-native
Install
bun add bod-dbServer
import { BodDB, increment, serverTimestamp, ref } from 'bod-db';
const db = new BodDB({
path: './data.db',
rules: {
'users/$uid': { read: true, write: "auth.uid === $uid" },
},
indexes: { 'users': ['role', 'createdAt'] },
sweepInterval: 60000, // TTL sweep every 60s
fts: {}, // enable full-text search
vectors: { dimensions: 384 }, // enable vector search
});
// CRUD
db.set('users/u1', { name: 'Alice', role: 'admin' });
db.get('users/u1'); // { name: 'Alice', role: 'admin' }
db.update('users/u1', { name: 'Bob' }); // shallow merge
db.updateMulti({ 'users/u1/name': 'Bob', 'counters/visits': 42 });
db.delete('users/u1');
// Transforms
db.set('counters/visits', increment(1)); // atomic increment
db.set('posts/p1/updatedAt', serverTimestamp()); // server timestamp
db.set('posts/p1/author', ref('users/u1')); // reference
// Push (append-only with time-sortable key)
const key = db.push('logs', { level: 'info', msg: 'started' });
// TTL
db.set('sessions/temp', { token: 'abc' }, { ttl: 3600 }); // expires in 1h
// Transaction
db.transaction((tx) => {
const user = tx.get('users/u1');
tx.set('users/u1/lastLogin', Date.now());
tx.update('stats/logins', { total: 1 });
});
// Query
db.query('users')
.where('role', '==', 'admin')
.order('name')
.limit(10)
.get();
// Full-text search
db.index('posts/p1', 'Hello world tutorial');
db.search({ text: 'hello', path: 'posts', limit: 10 });
// Vector search
db.vectors!.store('docs/d1', embedding);
db.vectorSearch({ query: embedding, path: 'docs', limit: 5, threshold: 0.7 });
// Subscribe
const off = db.on('users/u1', (snap) => console.log(snap.val()));
db.onChild('users', (e) => console.log(e.type, e.key, e.val()));
// Serve over network
db.serve({ port: 4400 });Client
import { BodClient } from 'bod-db/client';
const client = new BodClient({
url: 'ws://localhost:4400',
auth: () => 'my-token',
reconnect: true,
});
await client.connect();
// CRUD
await client.set('users/u1', { name: 'Alice' });
const user = await client.get('users/u1');
// Batch (atomic multi-op)
await client.batch([
{ op: 'set', path: 'users/u1/name', value: 'Bob' },
{ op: 'delete', path: 'users/u2' },
]);
// Push
const key = await client.push('logs', { msg: 'hello' });
// Subscribe
const off = client.on('users/u1', (snap) => console.log(snap.val()));
client.onChild('users', (e) => console.log(e.type, e.key));
client.disconnect();BodClientCached (Browser Cache)
import { BodClient, BodClientCached } from 'bod-db/client';
const client = new BodClient({ url: 'ws://localhost:4400' });
await client.connect();
const cached = new BodClientCached(client, {
maxMemoryEntries: 500, // LRU eviction cap
maxAge: 7 * 24 * 3600000, // IDB TTL (7 days)
});
await cached.init();
// Stale-while-revalidate: instant cache hit, background refetch
const val = await cached.get('users/u1');
// Subscriptions keep cache fresh automatically
const off = cached.on('users/u1', (snap) => console.log(snap.val()));
// Writes invalidate cache (path + ancestors)
await cached.set('users/u1/name', 'Bob');
// Warmup on page load
await cached.warmup(['users/u1', 'config/app']);Streams (Kafka-like)
// Push events to a topic
db.push('events/orders', { orderId: 'o1', amount: 250 });
// Consumer groups — each group tracks its own offset (fan-out)
const events = db.stream.read('events/orders', 'billing', 50);
db.stream.ack('events/orders', 'billing', events.at(-1)!.key);
// Compact old events into a snapshot
db.stream.compact('events/orders', { maxCount: 1000, keepKey: 'orderId' });
const view = db.stream.materialize('events/orders', { keepKey: 'orderId' });
// Client
const reader = client.stream('events/orders', 'billing');
const unsub = reader.on((events) => { /* live delivery */ });Message Queue (SQS-like)
// Server
const db = new BodDB({ mq: { visibilityTimeout: 30, maxDeliveries: 5 } });
db.mq.push('queues/jobs', { type: 'email', to: '[email protected]' });
const jobs = db.mq.fetch('queues/jobs', 5); // claim up to 5
db.mq.ack('queues/jobs', jobs[0].key); // done — delete
db.mq.nack('queues/jobs', jobs[0].key); // failed — back to pending
db.mq.peek('queues/jobs'); // view without claiming
db.mq.dlq('queues/jobs'); // dead letter queue
db.mq.purge('queues/jobs'); // delete pending
db.mq.purge('queues/jobs', { all: true }); // delete all (pending + inflight + DLQ)
// Client
const q = client.mq('queues/jobs');
await q.push({ type: 'email' });
const msgs = await q.fetch(5);
await q.ack(msgs[0].key);Replication
// Primary — emits writes to _repl stream
const primary = new BodDB({ replication: { role: 'primary' } });
primary.serve({ port: 4400 });
await primary.replication!.start();
// Replica — reads local, writes proxied to primary
const replica = new BodDB({
replication: { role: 'replica', primaryUrl: 'ws://primary:4400', replicaId: 'r1' },
});
await replica.replication!.start();
// Multi-source — pull specific paths from multiple remote DBs
const aggregator = new BodDB({
replication: {
role: 'primary',
sources: [
{ url: 'ws://db-a:4400', paths: ['catalog'], localPrefix: 'a', id: 'src-a' },
{ url: 'ws://db-b:4400', paths: ['alerts'], localPrefix: 'b', id: 'src-b' },
],
},
});
await aggregator.replication!.start();
// Remote catalog/item → local a/catalog/item
// Per-path topology — mixed ownership per path prefix
const edge = new BodDB({
replication: {
role: 'primary',
primaryUrl: 'ws://central:4400',
paths: [
{ path: '_vfs', mode: 'primary' }, // local authoritative
{ path: '_auth', mode: 'replica' }, // remote authoritative
{ path: 'config', mode: 'sync' }, // bidirectional
{ path: 'telemetry', mode: 'writeonly' }, // push-only
],
},
});Rules
Function-based, expression strings, or JSON config files:
// Inline
const db = new BodDB({
rules: {
'users/$uid': {
read: true,
write: "auth.uid === $uid",
},
'admin': {
read: "auth.role == 'admin'",
write: (ctx) => ctx.auth?.role === 'admin',
},
},
});
// From JSON file
const db2 = new BodDB({ rules: './rules.json' });
// rules.json: { "rules": { "users/$uid": { "read": true, "write": "auth.uid === $uid" } } }
// From TS file (async)
const db3 = await BodDB.create({ rules: './rules.ts' });Expression rules support: auth.*, $wildcard params, data/newData, comparisons (==, !=, >=, <, etc.), logical ops (&&, ||, !), parens, null checks. Safe AST evaluation — no eval.
REST API
GET /db/users/u1 → { ok: true, data: { name: 'Alice' } }
PUT /db/users/u1 → body: JSON → { ok: true }
DELETE /db/users/u1 → { ok: true }SSE
GET /sse/users/u1 → text/event-stream (value events)
GET /sse/users?event=child → text/event-stream (child events)React Hooks
import { useValue, useChildren, useQuery, useMutation } from 'bod-db/react';
const { data, loading } = useValue(client, 'users/u1');
const { children, loading } = useChildren(client, 'users');
const { data, loading } = useQuery(client, 'users', { filters: [...], limit: 10 });
const { set, update, del, loading } = useMutation(client);File Adapter
import { FileAdapter } from 'bod-db';
const adapter = new FileAdapter(db, {
root: './uploads',
basePath: 'files',
watch: true,
metadata: true,
});
await adapter.start();
// Files synced to db as: files/<relPath> → { size, mtime, mime }Benchmark
bun run tests/bench.ts| Workload | ops/sec | p50 | p99 | |---|---|---|---| | Read-only | ~200K | 0.00ms | 0.03ms | | Read-heavy (95/5) | ~170K | 0.00ms | 0.04ms | | Update-heavy (50/50) | ~94K | 0.01ms | 0.07ms | | Insert-heavy | ~89K | 0.01ms | 0.05ms | | Subscription (100 subs) | ~3.5K | 0.03ms | 4ms |
Test
bun test # 228 tests