Integrations
bunostrdb is a storage engine, not a network client: there is no relay, websocket, or signer code inside. Every integration follows one pattern — events in as JSON, answers out as objects — over the library or the CLI, whichever fits the host.
Relay → database
Bring your own relay connection (any library, any runtime that can call into Bun). Forward each EVENT envelope to the background parser; batch backfills through processEvents().
// @ts-nocheck
import { buildDraftNote, BunNdb } from "bunostrdb";
{
using ndb = new BunNdb("./data.relay");
// Wire this to any relay library — one call per EVENT envelope.
function onRelayEvent(subId: string, event: unknown): void {
ndb.processEvent(JSON.stringify(["EVENT", subId, event]));
}
// Demo with a local signed event standing in for a relay message.
const { event } = buildDraftNote("hello from the relay");
onRelayEvent("sub1", event);
await ndb.waitForNoteById(event.id);
// Backfill batch as NDJSON (one envelope per line) in a single call.
const more = [buildDraftNote("backfill one"), buildDraftNote("backfill two")];
const ndjson = more.map((draft) => JSON.stringify(["EVENT", "sub1", draft.event])).join("\n");
ndb.processEvents(ndjson);
for (const draft of more) await ndb.waitForNoteById(draft.id);
console.log("relay events committed:", more.length + 1);
}Pair it with a subscription to know when forwarded events are committed:
// @ts-nocheck
import { buildDraftNote, BunNdb, FilterBuilder } from "bunostrdb";
{
using ndb = new BunNdb("./data.relay-live");
const subId = ndb.subscribe([new FilterBuilder().kinds([1])]);
const { event, id } = buildDraftNote("hello live world");
ndb.processEvent(JSON.stringify(["EVENT", "sub1", event]));
await ndb.waitForNotes(subId);
console.log(ndb.pollForNotes(subId, 128), id);
}CLI scripting
Every subcommand reads --db <dir> (default ./data, auto-created) and the JSON-accepting options take a literal document, a file path, or - for stdin — so the CLI composes with pipes and cron:
Run these commands directly from the repository root after installing dependencies and building the native library (see Getting started). They work offline and create their inputs and database under ./cli-demo. Choose the version for your shell:
mkdir -p ./cli-demo
# A local producer supplies one signed relay EVENT envelope on stdin.
bun -e '
import { buildDraftNote } from "bunostrdb";
const { event } = buildDraftNote("hello from a relay envelope");
console.log(JSON.stringify(["EVENT", "demo", event]));
' | bun run cli -- ingest --db ./cli-demo/data --event -
# Draft commands wait for storage, so the following lookups have real data.
bun run cli -- draft-note --db ./cli-demo/data --content "hello from the CLI" > ./cli-demo/note.json
bun run cli -- draft-profile --db ./cli-demo/data --name "CLI demo" > ./cli-demo/profile.json
# Create the filter file before using it.
printf '%s\n' '{"kinds":[1]}' > ./cli-demo/timeline.json
bun run cli -- query --db ./cli-demo/data --filter ./cli-demo/timeline.json --limit 50
# Extract actual IDs from the draft commands instead of filling placeholders.
note_id=$(bun -e 'console.log((await Bun.file(process.argv[1]).json()).id)' ./cli-demo/note.json)
pubkey=$(bun -e 'console.log((await Bun.file(process.argv[1]).json()).event.pubkey)' ./cli-demo/profile.json)
bun run cli -- get-note --db ./cli-demo/data --id "$note_id"
bun run cli -- get-profile --db ./cli-demo/data --pubkey "$pubkey"mkdir -p ./cli-demo
# A local producer supplies one signed relay EVENT envelope on stdin.
bun -e '
import { buildDraftNote } from "bunostrdb";
const { event } = buildDraftNote("hello from a relay envelope");
console.log(JSON.stringify(["EVENT", "demo", event]));
' | bun run cli -- ingest --db ./cli-demo/data --event -
# Draft commands wait for storage, so the following lookups have real data.
bun run cli -- draft-note --db ./cli-demo/data --content "hello from the CLI" > ./cli-demo/note.json
bun run cli -- draft-profile --db ./cli-demo/data --name "CLI demo" > ./cli-demo/profile.json
# Create the filter file before using it.
printf '%s\n' '{"kinds":[1]}' > ./cli-demo/timeline.json
bun run cli -- query --db ./cli-demo/data --filter ./cli-demo/timeline.json --limit 50
# Extract actual IDs from the draft commands instead of filling placeholders.
set note_id (bun -e 'console.log((await Bun.file(process.argv[1]).json()).id)' ./cli-demo/note.json)
set pubkey (bun -e 'console.log((await Bun.file(process.argv[1]).json()).event.pubkey)' ./cli-demo/profile.json)
bun run cli -- get-note --db ./cli-demo/data --id "$note_id"
bun run cli -- get-profile --db ./cli-demo/data --pubkey "$pubkey"For inline criteria, the query also accepts --filter '{"kinds":[1]}'. A file path must point to an existing JSON file.
ingest --event - reads one JSON document to EOF, such as a single ["EVENT", "subscription-id", event] envelope. It reports asynchronous acceptance, which does not guarantee validation or storage. A live relay connection produces multiple messages (including EOSE and NOTICE), so do not pipe its entire stream into this command. For a continuous relay feed or NDJSON batch, use processEvent() / processEvents() as shown above.
From Node
The published bunostrdb-cli package runs from Node too: its bunostrdb binary is a small Node shim that spawns Bun with the CLI entry (override the Bun binary with BUN_BIN=<path>). From the repository root, create the input file and then run the CLI with npx:
mkdir -p ./cli-demo
bun -e '
import { buildDraftNote } from "bunostrdb";
const { event } = buildDraftNote("hello from the Node CLI");
console.log(JSON.stringify(["EVENT", "demo", event]));
' > ./cli-demo/event.json
npx --package=bunostrdb-cli bunostrdb ingest --db ./cli-demo/data --event ./cli-demo/event.jsonThese commands work in Bash and Fish. --event needs an existing JSON file, inline JSON, or - for stdin. A missing event.json is treated as inline input and fails JSON parsing.
The package name is bunostrdb-cli; its executable is bunostrdb.
Local-first apps
The drafts module is the app pattern for data that must never leave the device: mint an ephemeral identity per draft (or pin one via createDraftEvent({ secret })), sign locally, ingest, and search. See Personal draft DB (CLI) for the full walkthrough.
// @ts-nocheck
import { BunNdb, createDraftEvent, ingestDraft } from "bunostrdb";
{
using ndb = new BunNdb("./data.app");
// One stable identity across drafts instead of the default ephemeral one
// (`secret` is a 32-byte Buffer — store it wherever the app keeps secrets).
const secret = Buffer.from("11".repeat(32), "hex");
const draft = createDraftEvent({ secret, content: "my app draft" });
const id = await ingestDraft(ndb, draft.eventJson);
console.log("stored app draft", id);
}Full-text search in products
textSearch(term, { filter?, order?, limit? }) is ranked standalone search (limit capped at 128). Scope it with a native Filter for per-author / per-kind search boxes; use FilterBuilder.search() + query() when the search must compose with other predicates.
