# 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 ; } function Search() { const db = useSpeedyDb(); // Snapshot fields spread flat + stable-identity methods return <> db.search(e.target.value)} /> ; } ``` Vue (`/vue`), Svelte (`/svelte`), Solid (`/solid`), and Angular (`/angular`) ship the same store as their idiomatic binding — each an optional peer. --- ## SpeedyDbClient API (complete) Constructor: `new SpeedyDbClient(options?)`. Store: - `subscribe(fn: (snapshot) => void): () => void` — fires on every mutation. - `getSnapshot(): Snapshot` — current immutable, referentially-stable snapshot. - `getServerSnapshot(): Snapshot` — frozen all-cold snapshot for SSR/hydration. - `on(event, cb): () => void` — typed events (below). - `info()` — resolved config + asset base + every derived asset path. - `configure({ agent?, embedder? })` — stash model overrides BEFORE the first load. Engine & persistence: - `warm(): Promise` — lazy engine init (idempotent). - `warmIfStored(): Promise` — warm only if a prior durable store exists. - `flush(): void` — best-effort durable flush (bind to `pagehide`). - `reset(): Promise` — wipe the durable store, return to cold. - `db(): WasmBrowserDb | null` — the live engine (for advanced/direct use). Ingest: - `ingestFile(file): Promise` · `ingestSample({name,src}): Promise` - `ingestMediaSample(name): Promise` — a sample whose extraction ships in the constructor's `mediaSamples` map. Sources & scope: - `sourceOfRow(rowId): SourceEntry | null` · `sourceByName(name): SourceEntry | null` - `setSourceSelected(key, selected): void` — scope a file in/out (keeps its rows). - `setActiveSource(key | null): void` — the previewed source. - `selectedOnly(hits)` — filter hits to selected sources, tagging each with `_src`. Models: - `loadSemantic(): Promise` — start the approved embedder + backfill vectors. - `prefetchAgent(): Promise` — start the agent model download. Query: - `setMode("rag" | "agent"): void` — active mode (each keeps its own result buffer). - `search(query): void` — hybrid RAG into the rag buffer (reactive; debounce yourself). - `searchFiles(query, limit = 12): Promise<{src,score,row_id,found}[]>` — unique files. - `retrieveAll(query): Promise` — one-shot merged hits. - `ask(query): Promise` — retrieve → stream a grounded answer → widen once on low confidence. Single-flight (`snapshot.agent.busy` guards re-entry). - `expandRow(rowId, radius): Promise` — ± neighboring rows for a hit. Teardown: - `dispose(): Promise` — detach listeners, revoke object URLs, close + flush the store. ### Typed events (`on(name, cb)`) `change` (full snapshot) · `engine` · `pipe` · `sources` · `semantic` · `results` `{mode,query,hits,upgraded,noneReason}` · `agent` `{state,pct,busy,device,phase,status}` · `agent:token` `{text}` · `mode` · `error` `{name,message}`. ### Snapshot shape (what your UI reads) `{ engineStatus, durable, storage, restoredRowCount, pipeStage, engaged, sources[], activeSourceKey, anySelected, selectedNames, semantic{state,pct,draining,message}, searchMode, ragResult{query,hits,noneReason}, agentResult{phase,status,answer,streaming,passages,lowConfidence}, agent{state,pct,busy,device}, unseenRag, unseenAgent, lastError }` Enums: `engineStatus` = cold|warming|live|live-mem|unavailable · `storage` = opfs|idb|memory · `semantic.state`/`agent.state` = off|loading|ready|failed · `agentResult.phase` = idle|retrieving|generating|widening|done. --- ## Options (`SpeedyDbOptions`) | Option | Meaning | |---|---| | `dbName` | IndexedDB/OPFS store name; null/"" = in-memory only | | `assetBase` | **Required in bundled apps** — the `copy-assets` output dir | | `agent` | agent model overrides, e.g. `{ model: "minicpm5-1b" }` (see below) | | `embedder` | embedder overrides — **do not change the model** | | `mediaSamples` | `{ [name]: { type, src, frames?, chunks } }` for `ingestMediaSample` | | `enginePath` / `wasmPath` / `agentPath` / `embedderPath` / `durablePath` / `extractPath` | per-asset overrides on top of `assetBase` | | tuning | `chunkBytes`, `maxChunks`, `maxUploadBytes`, `semanticWaitMs`, `maxAgentPassages`, `widenRadius`, `maxAgentContext` … (sensible `DEFAULTS` — leave unless you know why) | Agent model presets (via `configure({ agent: { model } })` or the `agent` option): `minicpm5-1b` (default, ~0.7 GB) · `qwen3-0.6b` (~0.4 GB). Both are single-file Q4_K_M GGUFs run by wllama (llama.cpp) on the CPU — the same runtime as the embedder — so they work on any machine, GPU or not, and always run on the main thread (wllama needs `document`). Both are hybrid reasoners, and reasoning is disabled at the chat template (`enable_thinking: false`) — leave it on and the model spends its whole token budget reasoning and returns an EMPTY answer. Custom repo (ONNX text-generation, needs WebGPU): `configure({ agent: { modelId: "onnx-community/your-model-ONNX", arch: "causal" } })`. Live OPFS persistence: `new SpeedyDbClient()` runs on the **main thread** (IndexedDB/ memory). For true OPFS you run the client in a worker via `@speedydb/core/mirror` (`SpeedyDbClientMirror`) + `@speedydb/core/db-worker`, which expose the identical observable surface. Start with the main-thread client; adopt the mirror only if you need synchronous OPFS flushes. --- ## Hard rules & gotchas (an AI MUST follow these) - **Set `assetBase`** to the `copy-assets` output in any bundled app — otherwise the wasm/vendor runtime 404s and nothing loads. Verify with `client.info().assetBase`. - **Ingest before you search.** `warm()` first; `searchFiles`/`search` over an empty store return nothing (not an error). - **`search()` and `ask()` are reactive/fire-and-forget** (results land in the snapshot / events). **`searchFiles()`, `retrieveAll()`, `expandRow()` are Promises** that return data. Don't `await search()` expecting a result. - **One `ask()` at a time.** Check `snapshot.agent.busy`; a second call while busy is a no-op/queued — don't spam it. - **Debounce `search()` yourself** (~200 ms). The client seq-guards stale queries but won't throttle your keystrokes. - **Don't change the embedding model.** It's approved and pinned; a change wipes vectors. - **Downloads are large & deliberate.** Don't call `prefetchAgent()`/`ask()` on page load — trigger them on real user intent. The embedder auto-starts on `loadSemantic()`. - **Row ids: Number in hits, BigInt at the engine.** Use `hit.row_id` (Number) with client methods; never hand-convert. - **Teardown:** call `dispose()` (or React provider unmount does it) to release object URLs, listeners, and the durable lock; `flush()` on `pagehide` for a clean save. - **Errors are content-safe.** `error` events / `lastError` carry names + lengths only, never document text — safe to display. --- ## Reference - **TypeScript types** ship in the package (`@speedydb/core` → `types/*.d.ts`) — the authoritative signatures for `SpeedyDbClient`, options, hits, and the snapshot. - Entry points: `@speedydb/core` (barrel) · `/engine` · `/agent` · `/agent-worker` · `/embedder` · `/durable` · `/extract` · `/mirror` · `/db-worker` · `/react` `/vue` `/svelte` `/solid` `/angular`. - Deeper guides on this site: **/docs.html** (quick start, API, best practices) and the RAG-agent internals at **/docs/browser-rag-agent.html**. Live demo: **/testing.html**; browser dashboard: **/dashboard.html**.