Skip to content

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; initPromise dedupes concurrent initDb()

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

  1. SQL hybrid search in WASM Postgres instead of a JS-only vector store
  2. OR-term keyword query — converts plainto_tsquery & to | so any term can match
  3. Prefix-aware embeddings for E5/BGE models (queryPrefix / passagePrefix)
  4. Transactionally replace chunks on re-index (delete then insert)
  5. Export syncs UI prefs into settings before dumpDataDir()

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.