Skip to content

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
// @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
// @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:

bash
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"
fish
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:

bash
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.json

These 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
// @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.

Released under the BSD-3.0 License.