# SpeedyDb — on-device retrieval & RAG for the browser
> `@speedydb/core` is a framework-agnostic package that runs a real retrieval
> engine (Rust → WebAssembly), an approved on-device embedder, and an on-device
> RAG agent — **entirely in the browser**. You ingest files, search them (keyword
> + semantic), and ask a grounded question; retrieval, embedding, and generation
> all happen on the device and **nothing ever leaves the page**. This file is
> written for AI coding assistants: read it top-to-bottom and you can integrate
> SpeedyDb correctly without reading anything else.
Golden rules (read these first):
1. **Browser-only runtime.** Importing the package under Node/SSR never throws
(globals are read lazily), but the client only *works* in a browser tab.
2. **Host the runtime assets and set `assetBase`.** In any bundled app you MUST
run `npx speedydb-copy-assets` and pass the printed base to the client. Skipping
this is the #1 integration failure.
3. **One `SpeedyDbClient` = one observable store.** `subscribe()` + `getSnapshot()`
drive your UI; imperative async methods (`ingestFile`, `search`, `ask`, …) mutate it.
4. **Models download on first use, not on page load.** The embedder (~80 MB) starts
on `loadSemantic()`; the agent (~0.9 GB) on `prefetchAgent()`/`ask()`. Weights are
fetched from a CDN into the Cache API once, then reused. Never bundled.
5. **The agent answers ONLY from retrieved passages.** Empty retrieval returns
"nothing found" and never invokes the model. One generation at a time.
---
## TL;DR — a complete working integration
```sh
# distributed from speedydb.org, not a registry — npm installs a tarball URL directly
npm i https://speedydb.org/packages/speedydb-core-0.2.0.tgz
npx speedydb-copy-assets --out public/speedydb # copies the wasm/vendor runtime, prints assetBase
```
```js
import { SpeedyDbClient } from "@speedydb/core";
// 1 · Boot one client. dbName → IndexedDB persistence (omit/null = in-memory).
// assetBase = the directory speedydb-copy-assets wrote to (served publicly).
const db = new SpeedyDbClient({ dbName: "my-app", assetBase: "/speedydb/" });
await db.warm(); // engine up: durable-first, in-memory fallback
// 2 · Ingest. Text/CSV/MD/JSON ingest directly; PDF/DOCX/ODT extract on-device.
await db.ingestFile(file); // `file` is a File (from or drop)
await db.loadSemantic(); // optional: turn on meaning-based (vector) search
// 3a · Search → the matching FILES (deduped, ranked). Promise, returns data.
const files = await db.searchFiles("head gasket torque", 12);
// → [{ src: SourceEntry, score: number, row_id: number, found: "text"|"vector"|"both" }]
// 3b · Or ask the on-device agent for a grounded, streamed answer.
db.on("agent:token", ({ text }) => showLive(text)); // full answer-so-far per token
await db.ask("what is the head gasket torque?"); // loads the model on first call
const { answer, passages } = db.getSnapshot().agentResult; // answer + its cited sources
```
That is the whole happy path: **boot → ingest → search / ask**. Everything below
is detail.
---
## Install & host the runtime assets
The ~59 MB of runtime wasm (engine + vendored ONNX Runtime + vendored wllama)
ships **inside the npm tarball** — no CDN, no postinstall, works offline. It must
be served from your app's public directory, and the client must know where:
```sh
npx speedydb-copy-assets --out public/speedydb # → prints the assetBase to use, e.g. "/speedydb/"
npx speedydb-copy-assets --out public/speedydb --fingerprint # content-hashed dir for immutable caching
```
Pass that base to every client: `new SpeedyDbClient({ assetBase: "/speedydb/" })`.
Relative bases resolve against the client module; a missing trailing slash is added.
Per-path overrides (`wasmPath`, `agentPath`, `embedderPath`, …) layer on top if you
split assets across origins. `client.info()` returns the resolved base + every
derived path — log it once to confirm hosting is correct.
Peer deps pin the runtimes the vendored bundles were built against —
`@huggingface/transformers@4.2.0`, `@wllama/wllama@3.5.1`. Framework bindings add
optional peers (`react`, `vue`, `svelte`, `solid-js`, `@angular/core`).
---
## Core concepts (read once)
- **`SpeedyDbClient`** — the whole controller. An observable store
(`subscribe(fn)` / `getSnapshot()` — the snapshot is **referentially stable**
between mutations, safe for React `useSyncExternalStore`) plus imperative async
methods. Fine-grained `on(event, cb)` is optional; most apps only need `subscribe`.
- **Lifecycle:** `warm()` (engine up) → `ingestFile()` (chunk + store) →
`loadSemantic()` (embedder on, backfills vectors) → `search()`/`searchFiles()`/`ask()`.
Each load method is idempotent and retry-safe.
- **Persistence:** `dbName` set → durable (**OPFS** in a worker where available,
else **IndexedDB** write-behind); `dbName` null/"" → **in-memory** (resets on
reload). `snapshot.storage` reports which backend you got; `warmIfStored()`
auto-restores a returning visitor; `flush()` on `pagehide`.
- **On-device models:** the **embedder is fixed** (`nomic-ai/nomic-embed-text-v1.5`,
approved — do not change it; changing the embedding model wipes stored vectors).
The **agent model is selectable** (default `minicpm5-1b`; also `qwen3-0.6b`) — both
are CPU GGUFs run by wllama. They download on deliberate engagement, cached after first fetch.
- **Sources & scope:** every ingested file is a `SourceEntry` (key, name, type,
rows, preview…). `setSourceSelected(key, false)` removes a file from search scope
**without deleting** its rows.
- **Hits:** a retrieval hit carries `row_id`, `text`, `score`, `found`
(`"text"|"vector"|"both"`), and `_src` (its owning `SourceEntry`). **Row ids are
plain Numbers in hits; `expandRow`/engine take BigInt — the client handles the
conversion, you never do.**
---
## Recipes
### Ingest
```js
await db.ingestFile(file); // File → SourceEntry (chunked + stored)
await db.ingestSample({ name: "notes.md", src: "/notes.md" }); // fetch + ingest a URL
```
`ingestFile` rejects with a typed `IngestError` (`err.code` ∈ `too-large` |
`unsupported` | `empty` | `no-text-layer` | `ingest-failed`, plus `err.userMessage`).
Route `unsupported` to a "preview only" affordance; surface the rest.
### Search — matching FILES (deduped, ranked) — best for a file browser
```js
const files = await db.searchFiles(query, 12); // Promise → up to 12 unique files
// each: { src: SourceEntry, score, row_id, found }. row_id = the file's best passage,
// so you can open a media preview at the matched page/frame (src.frames[...]).
```
### Search — passage hits, reactive (best for a live "as you type" list)
```js
db.on("results", ({ query, hits, noneReason }) => renderHits(hits)); // or use subscribe()
db.search(userInput); // fire-and-forget, debounce yourself; word hits land first,
// semantic hits merge in as the query embedding returns
```
`retrieveAll(query)` is the one-shot Promise version (returns the merged `Hit[]`).
### Ask the on-device agent (grounded answer + cited passages)
```js
db.on("agent:token", ({ text }) => setAnswer(text)); // stream: full text so far
await db.ask("does the coastal line run on Sundays?"); // single-flight; loads model on 1st call
const r = db.getSnapshot().agentResult;
// r.answer (final text), r.passages (the exact sources it received — render as citations),
// r.lowConfidence (true = a hedge / nothing-found). The answer cites passages as [1],[2]…
// → passages[n-1]. Call db.prefetchAgent() early to warm the ~0.9 GB model in the background.
```
### React
```jsx
import { SpeedyDbProvider, useSpeedyDb } from "@speedydb/core/react";
function App() {
return