03. Backend Deep Dive¶
There is no HTTP API server. This chapter covers the in-browser service layer: database, workers, RAG modules, and LLM runtime — the “backend” that the React UI calls directly.
Layout and entrypoints¶
| Module | Role |
|---|---|
src/db/client.ts |
Singleton PGlite init, export/import |
src/db/migrations.ts |
Schema v1 + migration_versions |
src/rag/indexing.ts |
Document indexing orchestration |
src/rag/retrieval.ts |
Hybrid retrieval + RRF |
src/rag/orchestrator.ts |
Ask pipeline |
src/rag/chunking.ts |
Token-estimate chunking |
src/rag/extractors/* |
Text/PDF extraction |
src/rag/embedding-*.ts |
Model catalog + provider |
src/llm/llm-runtime.ts |
Engine adapters + streaming |
src/workers/*.ts |
Off-main-thread work |
Boot: src/main.tsx calls initDb() before render.
Request lifecycle (logical)¶
graph TD
classDef default fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef highlight fill:#065f46,stroke:#34d399,stroke-width:2px,color:#f0fdf4
Route["Route / mutation"]:::highlight
Lib["lib/projects, document-files"]
Svc["rag/* or llm/*"]
W["Web Worker"]
DB["getDb() PGlite"]
Route --> Lib
Route --> Svc
Svc --> W
Svc --> DB
Lib --> DB
linkStyle default stroke:#64748b,stroke-width:2px
TanStack Query mutations on documents/projects call service functions; chat uses async generators rather than REST.
Service / repository boundaries¶
| Concern | Pattern |
|---|---|
| Projects | src/lib/projects.ts — SQL helpers over PGlite |
| Preferences | localStorage via src/lib/preferences.ts; synced into settings on export |
| Files | Separate IDB API in document-files.ts |
| Chunks/docs | Written inside indexDocument transactions |
| History | Inserted from chat route after completion |
No formal repository layer or DI container — modules import getDb() directly.
Async and concurrency¶
- PGlite queries are async on the main thread
- Indexing worker handles extract+chunk; main thread embeds via embedding worker then writes DB
- Embedding worker loads pipeline once per model; embeds texts sequentially in a loop
- LLM streaming is async generators; abort via engine
abort()/AbortSignal - Single DB singleton;
initPromisededupes concurrentinitDb()
Auth, permissions, validation¶
No user auth. Isolation is single-browser-profile storage. Validation is light: MIME/extension handling in extractors, project settings numeric fields in UI, backup file extension checks in settings.
Background jobs¶
No queue service. Indexing is foreground async work triggered by upload/retry mutations. index_jobs table exists in schema but is unused.
Error handling and logging¶
- Index errors →
documents.status = 'failed',error_message - Orchestrator catch →
{ type: 'error' } - DB init/import failures →
console.error/ thrown errors - No structured logging or telemetry pipeline
Notable implementation choices¶
- SQL hybrid search in WASM Postgres instead of a JS-only vector store
- OR-term keyword query — converts
plainto_tsquery&to|so any term can match - Prefix-aware embeddings for E5/BGE models (
queryPrefix/passagePrefix) - Transactionally replace chunks on re-index (delete then insert)
- Export syncs UI prefs into
settingsbeforedumpDataDir()
Backend interview questions¶
Q: Why PGlite instead of IndexedDB object stores for vectors?
A: SQL joins, GIN full-text indexes, pgvector distance operators, and transactional updates give a familiar RAG data plane without a server.
Q: Where does heavy work run?
A: Parse/chunk and embedding inference in Web Workers; vector SQL and LLM orchestration on the main thread (LLM libraries may use their own internals).
Q: How is embedding consistency enforced?
A: Projects store embedding_model_id; chunks store the model used; retrieval filters c.embedding_model_id = $model. Changing the project model without re-indexing is a soft risk.