docs/7-DEVELOPMENT/architecture.md
Open Notebook follows a three-tier architecture with clear separation of concerns:
┌─────────────────────────────────────────────────────────┐
│ Your Browser │
│ Access: http://your-server-ip:8502 │
└────────────────┬────────────────────────────────────────┘
│
▼
┌───────────────┐
│ Port 8502 │ ← Next.js Frontend (what you see)
│ Frontend │ Also proxies API requests internally!
└───────┬───────┘
│ proxies /api/* requests ↓
▼
┌───────────────┐
│ Port 5055 │ ← FastAPI Backend (handles requests)
│ API │
└───────┬───────┘
│
▼
┌───────────────┐
│ SurrealDB │ ← Database (internal, auto-configured)
│ (Port 8000) │
└───────────────┘
Key Points:
/api/* requests to the backend, simplifying reverse proxy setupAPI_URL=http://your-server-ip:5055Open Notebook is built on a three-tier, async-first architecture designed for scalability, modularity, and multi-provider AI flexibility. The system separates concerns across frontend, API, and database layers, with LangGraph powering intelligent workflows and Esperanto enabling seamless integration with 8+ AI providers.
Core Philosophy:
Purpose: Responsive, interactive user interface for research, notes, chat, and podcast management.
Technology Stack:
Key Responsibilities:
Communication Pattern:
http://localhost:5055 (dev) or environment-specific (prod)Component Architecture:
/src/app/: Next.js App Router (pages, layouts)/src/components/: Reusable React components (buttons, forms, cards)/src/hooks/: Custom hooks (useNotebook, useChat, useSearch)/src/lib/: Utility functions, API clients, validators/src/styles/: Global CSS, Tailwind configPurpose: RESTful backend exposing operations on notebooks, sources, notes, chat sessions, and AI models.
Technology Stack:
Architecture:
FastAPI App (main.py)
├── Routers (HTTP endpoints)
│ ├── routers/notebooks.py (CRUD operations)
│ ├── routers/sources.py (content ingestion, upload)
│ ├── routers/notes.py (note management)
│ ├── routers/chat.py (conversation sessions)
│ ├── routers/search.py (full-text + vector search)
│ ├── routers/transformations.py (custom transformations)
│ ├── routers/models.py (AI model configuration)
│ └── routers/*.py (11 additional routers)
│
├── Services (business logic)
│ ├── *_service.py (orchestration, graph invocation)
│ ├── command_service.py (async job submission)
│ └── middleware (auth, logging)
│
├── Models (Pydantic schemas)
│ └── models.py (validation, serialization)
│
└── Lifespan (startup/shutdown)
└── AsyncMigrationManager (database schema migrations)
Key Responsibilities:
Startup Flow:
.env environment variablesRequest-Response Cycle:
HTTP Request → Router → Service → Domain/Repository → SurrealDB
↓
LangGraph (optional)
↓
Response ← Pydantic serialization ← Service ← Result
Purpose: Graph database with built-in vector embeddings, semantic search, and relationship management.
Technology Stack:
.surql files in /migrations/ (auto-run on API startup)Core Tables:
| Table | Purpose | Key Fields |
|---|---|---|
notebook | Research project container | id, name, description, archived, created, updated |
source | Content item (PDF, URL, text) | id, title, full_text, topics, asset, created, updated |
source_embedding | Vector embeddings for semantic search | id, source, embedding, chunk_text, chunk_index |
note | User-created research notes | id, title, content, note_type (human/ai), created, updated |
chat_session | Conversation session | id, notebook_id, title, messages (JSON), created, updated |
transformation | Custom transformation rules | id, name, description, prompt, created, updated |
source_insight | Transformation output | id, source_id, insight_type, content, created, updated |
reference | Relationship: source → notebook | out (source), in (notebook) |
artifact | Relationship: note → notebook | out (note), in (notebook) |
Relationship Graph:
Notebook
↓ (referenced_by)
Source
├→ SourceEmbedding (1:many for chunked text)
├→ SourceInsight (1:many for transformation outputs)
└→ Note (via artifact relationship)
├→ Embedding (semantic search)
└→ Topics (tags)
ChatSession
├→ Notebook
└→ Messages (stored as JSON array)
Vector Search Capability:
source.full_text and note.contentConnection Management:
Python:
FastAPI:
Next.js:
React 19:
TypeScript:
SurrealDB:
Alternative Considered: PostgreSQL + pgvector (more mature but separate extensions)
Esperanto Library:
Alternative Considered: LangChain's provider abstraction (more verbose, less flexible)
LangGraph is a state machine library that orchestrates multi-step AI workflows. Open Notebook uses five core workflows:
open_notebook/graphs/source.py)Purpose: Ingest content (PDF, URL, text) and prepare for search/insights.
Flow:
Input (file/URL/text)
↓
Extract Content (content-core library)
↓
Clean & tokenize text
↓
Generate Embeddings (Esperanto)
↓
Create SourceEmbedding records (chunked + indexed)
↓
Extract Topics (LLM summarization)
↓
Save to SurrealDB
↓
Output (Source record with embeddings)
State Dict:
{
"content_state": {"file_path" | "url" | "content": str},
"source_id": str,
"full_text": str,
"embeddings": List[Dict],
"topics": List[str],
"notebook_ids": List[str],
}
Invoked By: Sources API (POST /sources)
open_notebook/graphs/chat.py)Purpose: Conduct multi-turn conversations with AI model, referencing notebook context.
Flow:
User Message
↓
Build Context (selected sources/notes)
↓
Add Message to Session
↓
Create Chat Prompt (system + history + context)
↓
Call LLM (via Esperanto)
↓
Stream Response
↓
Save AI Message to ChatSession
↓
Output (complete message)
State Dict:
{
"session_id": str,
"messages": List[BaseMessage],
"context": Dict[str, Any], # sources, notes, snippets
"response": str,
"model_override": Optional[str],
}
Key Features:
build_context_for_chat() utilityInvoked By: Chat API (POST /chat/execute)
open_notebook/graphs/ask.py)Purpose: Answer user questions by searching sources and synthesizing responses.
Flow:
User Question
↓
Plan Search Strategy (LLM generates searches)
↓
Execute Searches (vector + text search)
↓
Score & Rank Results
↓
Provide Answers (LLM synthesizes from results)
↓
Stream Responses
↓
Output (final answer)
State Dict:
{
"question": str,
"strategy": SearchStrategy,
"answers": List[str],
"final_answer": str,
"sources_used": List[Source],
}
Streaming: Uses astream() to emit updates in real-time (strategy → answers → final answer)
Invoked By: Search API (POST /ask with streaming)
open_notebook/graphs/transformation.py)Purpose: Apply custom transformations to sources (extract summaries, key points, etc).
Flow:
Source + Transformation Rule
↓
Generate Prompt (Jinja2 template)
↓
Call LLM
↓
Parse Output
↓
Create SourceInsight record
↓
Output (insight with type + content)
Example Transformations:
Invoked By: Sources API (POST /sources/{id}/insights)
open_notebook/graphs/prompt.py)Purpose: Generic LLM task execution (e.g., auto-generate note titles, analyze content).
Flow:
Input Text + Prompt
↓
Call LLM (simple request-response)
↓
Output (completion)
Used For: Note title generation, content analysis, etc.
Located in open_notebook/ai/models.py, ModelManager handles:
Usage:
from open_notebook.ai.provision import provision_langchain_model
# Get best LLM for context size
model = await provision_langchain_model(
task="chat", # or "search", "extraction"
model_override="anthropic/claude-opus-4", # optional
context_size=8000, # estimated tokens
)
# Invoke model
response = await model.ainvoke({"input": prompt})
LLM Providers:
Embedding Providers:
TTS Providers:
Every LangGraph invocation accepts a config parameter to override models:
result = await graph.ainvoke(
input={...},
config={
"configurable": {
"model_override": "anthropic/claude-opus-4" # Use Claude instead
}
}
)
Domain Objects (open_notebook/domain/):
Notebook: Research container with relationships to sources/notesSource: Content item (PDF, URL, text) with embeddingsNote: User-created or AI-generated research noteChatSession: Conversation history for a notebookTransformation: Custom rule for extracting insightsRepository Pattern:
open_notebook/database/repository.py)repo_query(): Execute SurrealQL queriesrepo_create(): Insert recordsrepo_upsert(): Merge recordsrepo_delete(): Remove recordsEntity Methods:
# Domain methods (business logic)
notebook = await Notebook.get(id)
await notebook.save()
notes = await notebook.get_notes()
sources = await notebook.get_sources()
All I/O is async:
await repo_query(...)await model.ainvoke(...)await upload_file.read()await graph.ainvoke(...)Benefits:
Example:
@router.post("/sources")
async def create_source(source_data: SourceCreate):
# All operations are non-blocking
source = Source(title=source_data.title)
await source.save() # async database operation
await graph.ainvoke({...}) # async LangGraph invocation
return SourceResponse(...)
Services orchestrate domain objects, repositories, and workflows:
# api/notebook_service.py
class NotebookService:
async def get_notebook_with_stats(notebook_id: str):
notebook = await Notebook.get(notebook_id)
sources = await notebook.get_sources()
notes = await notebook.get_notes()
return {
"notebook": notebook,
"source_count": len(sources),
"note_count": len(notes),
}
Responsibilities:
For long-running operations (ask workflow, podcast generation), stream results as Server-Sent Events:
@router.post("/ask", response_class=StreamingResponse)
async def ask(request: AskRequest):
async def stream_response():
async for chunk in ask_graph.astream(input={...}):
yield f"data: {json.dumps(chunk)}\n\n"
return StreamingResponse(stream_response(), media_type="text/event-stream")
For async background tasks (source processing), use Surreal-Commands job queue:
# Submit job
command_id = await CommandService.submit_command_job(
app="open_notebook",
command="process_source",
input={...}
)
# Poll status
status = await source.get_status()
Example:
// Frontend
const response = await fetch("http://localhost:5055/sources", {
method: "POST",
body: formData, // multipart/form-data for file upload
});
const source = await response.json();
Example:
# API
result = await repo_query(
"SELECT * FROM source WHERE notebook = $notebook_id",
{"notebook_id": ensure_record_id(notebook_id)}
)
Example:
# API
model = await provision_langchain_model(task="chat")
response = await model.ainvoke({"input": prompt})
/commands/{id} endpointExample:
# Submit async source processing
command_id = await CommandService.submit_command_job(...)
# Client polls status
response = await fetch(f"http://localhost:5055/commands/{command_id}")
status = await response.json() # returns { status: "running|queued|completed|failed" }
Tables (20+):
archived flag)Migrations:
/migrations/ directory_sbl_migrations table_down.surql files (manual)Graph Relationships:
Notebook
← reference ← Source (many:many)
← artifact ← Note (many:many)
Source
→ source_embedding (one:many)
→ source_insight (one:many)
→ embedding (via source_embedding)
ChatSession
→ messages (JSON array in database)
→ notebook_id (reference to Notebook)
Transformation
→ source_insight (one:many)
Query Example (get all sources in a notebook with counts):
SELECT id, title,
count(<-reference.in) as note_count,
count(<-embedding.in) as embedded_chunks
FROM source
WHERE notebook = $notebook_id
ORDER BY updated DESC
All I/O operations are non-blocking to maximize concurrency and responsiveness.
Trade-off: Slightly more complex code (async/await syntax) vs. high throughput.
Built-in support for 8+ AI providers prevents vendor lock-in.
Trade-off: Added complexity in ModelManager vs. flexibility and cost optimization.
LangGraph state machines for complex multi-step operations (ask, chat, transformations).
Trade-off: Steeper learning curve vs. maintainable, debuggable workflows.
SurrealDB for graph + vector search in one system (no external dependencies).
Trade-off: Operational responsibility vs. simplified architecture and cost savings.
Async job submission (source processing, podcast generation) prevents request timeouts.
Trade-off: Eventual consistency vs. responsive user experience.
archived field (data not removed, just marked inactive)/data/sqlite-db//data/uploads/ directory (not database)estimate_tokens() utility/commands/{id} for async operationsopen_notebook/graphs/workflow_name.py.add_node() / .add_edge()api/workflow_service.pyapi/main.pytests/test_workflow.pyopen_notebook/domain/model_name.pysave(), get(), delete() methods (CRUD)migrations/api/Open Notebook's architecture provides a solid foundation for privacy-focused, AI-powered research. The separation of concerns (frontend/API/database), async-first design, and multi-provider flexibility enable rapid development and easy deployment. LangGraph workflows orchestrate complex AI tasks, while Esperanto abstracts provider details. The result is a scalable, maintainable system that puts users in control of their data and AI provider choice.