backend / notebooklm-web

notebooklm-web

One Hono + tRPC server, mounted by both a Next.js web app and an Electron desktop app, that ingests documents into pgvector and runs retrieval, deep research and studio generation over them.

2026 · paused · open source

web → hono: tRPC / SSE / AI SDK streamdesk → hono: tRPC / SSE (localhost)hono → core: function calls via PlatformAdapterdesk → workers: postMessage RPCcore → pg: Drizzle SQL, <=> cosinecore → blob: StorageProvidercore → llm: streamText / generateObject / embedcore → search: HTTPScore → tts: HTTPS, pMap segmentsNext.js web appNext.js web app (client) Vercel, catch-all mounts HonoElectron desktopElectron desktop (client) Vite middleware / esbuild bundleHono app + tRPCHono app + tRPC (service) 36 procedures, 9 stream/one-shot routesUtility-processworkersUtility-process workers (worker) Kokoro TTS + local embeddings (ONNX)packages/corepackages/core (service) ingest, retrieve, notebook-research, agent harnessNeon Postgres / PGliteNeon Postgres / PGlite (store) 19 tables, 5 pgvector sibling tablesS3 / R2 / local FSS3 / R2 / local FS (store) uploaded PDFs, audio13 LLM / embeddingproviders13 LLM / embedding providers (model) via AI SDK factoryExa / Tavily / SearxNGExa / Tavily / SearxNG (third party) deep-research web searchDeepgram AuraDeepgram Aura (third party) web TTStRPC / SSE / AI SDK s…tRPC / SSE (localhost)function calls via Pl…postMessage RPCDrizzle SQL, <=> cosi…StorageProviderstreamText / generate…HTTPSHTTPS, pMap segments web → hono: tRPC / SSE / AI SDK streamdesk → hono: tRPC / SSE (localhost)hono → core: function calls via PlatformAdapterdesk → workers: postMessage RPCcore → pg: Drizzle SQL, <=> cosinecore → blob: StorageProvidercore → llm: streamText / generateObject / embedcore → search: HTTPScore → tts: HTTPS, pMap segmentsNext.js web appNext.js web app (client) Vercel, catch-all mounts HonoElectron desktopElectron desktop (client) Vite middleware / esbuild bundleHono app + tRPCHono app + tRPC (service) 36 procedures, 9 stream/one-shot routesUtility-processworkersUtility-process workers (worker) Kokoro TTS + local embeddings (ONNX)packages/corepackages/core (service) ingest, retrieve, notebook-research, agent harnessNeon Postgres /PGliteNeon Postgres / PGlite (store) 19 tables, 5 pgvector sibling tablesS3 / R2 / local FSS3 / R2 / local FS (store) uploaded PDFs, audio13 LLM / embeddingproviders13 LLM / embedding providers (model) via AI SDK factoryExa / Tavily /SearxNGExa / Tavily / SearxNG (third party) deep-research web searchDeepgram AuraDeepgram Aura (third party) web TTStRPC / SSE / AI SDK s…tRPC / SSE (localhost)function calls via Pl…postMessage RPCDrizzle SQL, <=> cosi…StorageProviderstreamText / generate…HTTPSHTTPS, pMap segments
clientserviceworkerstoremodelthird partyasync
  • [*]LanguagesTypeScript
  • [*]FrameworksHono · tRPC v11 · Next.js 16 · Electron 33 · Drizzle ORM · Better Auth · AI SDK v6 · Claude Agent SDK
  • [*]StoresPostgres (Neon) · pgvector · PGlite · S3 / R2 / Supabase Storage · local filesystem
  • [*]AsyncSSE streams · in-process job registry with AbortController cancellation · Electron utility-process workers · jsonb progress columns polled over tRPC
  • [*]InfraVercel · Electron (electron-builder installers via GitHub Actions) · Docker (SearxNG auto-start) · Bun workspaces
  • [*]Third partiesOpenAI · Anthropic · Google Gemini · Mistral · Cohere · Voyage · Groq · Ollama · OpenRouter · Together AI · xAI · Deepgram Aura · Exa · Tavily · SearxNG

A self-hostable NotebookLM built over 136 commits in about three weeks (21 April to 12 May 2026), all by the owner. The backend is a single framework-agnostic Hono app (packages/server/src/app.ts) that takes a PlatformAdapter carrying the Drizzle client, the Better Auth instance, the storage provider and env. The web build mounts it behind a Next.js catch-all route on Vercel with Neon Postgres and S3/R2/Supabase storage. The desktop build mounts the same app over embedded PGlite (with the pgvector extension) and the local filesystem.

The server exposes 36 tRPC procedures across 10 routers, plus 9 Hono routes for streaming and one-shot work: AI SDK chat streaming, SSE deep research, SSE audio overviews, upload, re-embed, studio cancel, quiz summary, an admin migration step runner, and file serving. Embeddings live in five sibling tables, one per dimension (384, 768, 1024, 1536 and 3072). Retrieval routes to the table that matches the user's current model, so switching embedding models never touches existing vectors.

Studio outputs (mind maps, FAQs, quizzes, audio) all draw on a cached, citation-grounded research artifact. Its cache key is the SHA-256 of the notebook's chunk fingerprint, the chat model and the normalised query. Concurrent requests are deduplicated in process with an in-flight promise map, and across processes with a unique index. On desktop, ONNX work (Kokoro TTS, local sentence-transformer embeddings) runs in Electron utility processes behind a small request/response RPC with timeouts and respawn on exit.

There are no automated tests in the repo. The web database uses drizzle-kit push rather than tracked migrations, and the desktop runs idempotent CREATE TABLE IF NOT EXISTS DDL on every launch.

How it holds up

Auth
Better Auth sessions (email/password and OAuth) mounted at /api/auth/*. tRPC context resolves the session and every procedure is protectedProcedure (46 uses, 36 procedures). Notebook-scoped procedures call assertOwns(notebookId, userId) before touching rows. Per-user provider API keys are AES-256-GCM encrypted with userId as AAD.
Data
19 Drizzle tables in packages/core/src/db/schema.ts: 4 Better Auth tables, notebooks, sources, source_chunks, messages, deep_research_runs, studio_outputs, notebook_research_reports, user_provider_credentials, user_ai_config, and 5 chunk_embeddings_<dim> sibling tables (384/768/1024/1536/3072). No migration files: web uses drizzle-kit push, and the desktop runs IF NOT EXISTS DDL at boot.
Consistency
The research cache is deduplicated twice: in process, a second caller with the same key awaits the first caller's promise; across processes, a unique index on cache_key catches the duplicate, and the losing insert reads back the winner's row. The legacy-embedding backfill is one INSERT ... SELECT ... ON CONFLICT DO NOTHING, so re-running it is safe. Job cancellation is cooperative through an AbortController per job id.
Failure
withRetry gives exponential backoff with jitter, a shouldRetry veto (it skips retries on 401/403/404) and abort awareness. A research section that fails is retried once at half top-K, then kept as a confidence:'low' placeholder instead of being dropped. Desktop boot marks any 'generating' or 'running' rows orphaned by a crash as error. Worker RPCs time out after 5 minutes, reject all pending calls when the worker exits, and respawn on the next call. Postgres error reasons are lifted out of Drizzle's wrapper so the client sees them.
Testing
0 test files. CI is a single release-desktop.yml workflow that builds installers; there is no test or typecheck job.

Techniques

  1. One server, two runtimes, via a platform adapterThe whole backend is one Hono app factory that only sees a PlatformAdapter (db, auth, storage, env) and never reads process.env. The same routers serve the Vercel app (Neon + S3) and the offline desktop app (PGlite + local FS).packages/server/src/app.ts:31 packages/server/src/adapter.ts:14 apps/desktop/src/server/stub-adapter.ts:270
  2. Per-dimension embedding tables with model-scoped retrievalVectors are stored in five sibling tables keyed by dimension. Retrieval picks the table for the user's current model and filters by model name, so a model switch is non-destructive. A dual-write window still serves legacy 768-dim vectors, and an idempotent INSERT...SELECT ON CONFLICT backfill moves them. 3072-dim gets a btree index only, because pgvector caps HNSW at 2000 dims.packages/core/src/db/schema.ts:389 packages/core/src/retrieve.ts:151 packages/core/src/retrieve.ts:198 packages/core/src/db/migrate-backfill-embeddings.ts:15 packages/core/src/db/migrate-vector-indexes.ts:47
  3. Fingerprinted research cache with two-level dedupEvery studio kind generates from one research artifact. Its cache key hashes the ready chunk ids, token sum and embedding model plus the chat model and normalised query, so any source change invalidates it. Concurrent callers share one in-flight promise, and a unique-index collision falls back to reading the winner's row.packages/core/src/notebook-research.ts:140 packages/core/src/notebook-research.ts:169 packages/core/src/notebook-research.ts:417 packages/core/src/notebook-research.ts:761
  4. Cancellable, retried TTS fan-outAudio overviews render script segments with a sliding-window pMap: a new segment starts as soon as any finishes. Each segment is wrapped in withRetry (exponential backoff plus jitter, no retry on 4xx-shaped errors) and tied to a per-job AbortSignal, so a cancel request stops in-flight work.packages/server/src/handlers/studio/audio-overview.ts:465 packages/server/src/handlers/studio/audio-overview.ts:500 packages/core/src/concurrency/retry.ts:12 packages/core/src/jobs/registry.ts:68
  5. Crash-safe desktop jobs and worker RPCOn boot the desktop marks rows left 'generating' or 'running' by a crash as error, so the UI never polls a job that will not finish. ONNX inference runs in Electron utility processes behind an id-correlated RPC with per-call timeouts; when a worker exits, its pending calls are rejected and it respawns on the next call.apps/desktop/src/server/stub-adapter.ts:376 apps/desktop/electron/main.cjs:58
  6. User-bound encrypted provider credentialsPer-user API keys for 13 providers are stored as AES-256-GCM ciphertext, IV, auth tag and key version, with userId as additional authenticated data. A row copied onto another user fails decryption.packages/core/src/crypto/secret.ts:24 packages/core/src/crypto/secret.ts:47

Counted

  • 136commits
    git rev-list --count HEAD
  • 1 (hallelx2, 135 + 1 web-UI commit)authors
    git shortlog -sne HEAD
  • 34625TypeScript LOC (all tracked .ts/.tsx)
    git ls-files '*.ts' '*.tsx' | xargs wc -l | tail -1
  • 14794backend LOC (packages/server + packages/core .ts)
    git ls-files 'packages/server/*.ts' 'packages/core/*.ts' | xargs wc -l | tail -1
  • 19DB tables
    grep -c pgTable packages/core/src/db/schema.ts (minus import line)
  • 5pgvector sibling tables
    packages/core/src/db/schema.ts:389-456
  • 10 / 36tRPC routers / procedures
    grep protectedProcedure packages/server/src/trpc/routers | grep -v import | wc -l
  • 9Hono stream/one-shot routes (excl. auth + tRPC mounts)
    packages/server/src/app.ts:65-85
  • 9AI provider embed adapters
    git ls-files packages/core/src/ai/embed (excl. index/types)
  • 0test files
    git ls-files | grep -iE '\.(test|spec)\.' | wc -l
  • 2026-04-21 to 2026-05-12active dev window
    git log --reverse --format=%ci