# How Open Notebook Persists Chat History with LangGraph Checkpointing in SQLite

> Learn how Open Notebook uses LangGraph checkpointing with SqliteSaver to automatically persist chat history in SQLite Build resilient applications with seamless chat state management.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: how-to-guide
- Published: 2026-07-04

---

**Open Notebook uses LangGraph's SqliteSaver to automatically serialize conversation states into a SQLite database, enabling persistent chat histories across sessions without custom database logic.**

Open Notebook is an open-source project that manages AI conversations through LangGraph's checkpointing system. The implementation stores complete message histories in a dedicated SQLite file, allowing stateful interactions to resume seamlessly across API calls. This architecture leverages the `SqliteSaver` class from `langgraph.checkpoint.sqlite` to handle serialization and state management automatically.

## Configuring the SQLite Checkpoint Database

### Checkpoint File Location

The repository centralizes checkpoint configuration in [`open_notebook/config.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/config.py). This file defines a dedicated folder for SQLite storage and establishes the path to the checkpoint database.

```python

# open_notebook/config.py

DATA_FOLDER = "./data"
sqlite_folder = f"{DATA_FOLDER}/sqlite-db"
os.makedirs(sqlite_folder, exist_ok=True)
LANGGRAPH_CHECKPOINT_FILE = f"{sqlite_folder}/checkpoints.sqlite"

```

The `LANGGRAPH_CHECKPOINT_FILE` constant points to `data/sqlite-db/checkpoints.sqlite`, which serves as the persistent store for all LangGraph states across the application.

### Directory Initialization

The configuration ensures the directory exists at runtime using `os.makedirs` with `exist_ok=True`. This prevents startup errors when the data folder is missing while maintaining a predictable location for the database file.

## Initializing the SqliteSaver

Graphs that require persistence import `SqliteSaver` from `langgraph.checkpoint.sqlite` and bind it to the configured database file. In [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py), the implementation establishes a SQLite connection with specific threading parameters to support concurrent access.

```python

# open_notebook/graphs/chat.py

import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver
from open_notebook.config import LANGGRAPH_CHECKPOINT_FILE

conn = sqlite3.connect(LANGGRAPH_CHECKPOINT_FILE, check_same_thread=False)
memory = SqliteSaver(conn)

```

The same pattern appears in [`open_notebook/graphs/source_chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/source_chat.py) for source-specific chat implementations. The `check_same_thread=False` parameter is critical for FastAPI deployments, allowing multiple concurrent requests to safely share the same SQLite connection.

## Defining Conversation State with Messages

The chat graph defines its state using a `TypedDict` that includes a `messages` field annotated with `add_messages`. This annotation instructs LangGraph on how to merge new messages into existing conversation history during graph execution.

```python

# open_notebook/graphs/chat.py

from typing import Annotated

class ThreadState(TypedDict):
    messages: Annotated[list, add_messages]   # Persists the full message list

    notebook: Optional[Notebook]
    # ... additional fields

```

The `add_messages` reducer function ensures that when a node returns new messages, they are appended to the existing list rather than replacing it. This mechanism preserves the complete conversation thread across checkpoint saves.

## Compiling the Graph with Persistence

After defining nodes and edges, the graph is compiled with the `SqliteSaver` instance passed as the `checkpointer` parameter. This binding enables automatic state persistence at each step.

```python

# open_notebook/graphs/chat.py

agent_state = StateGraph(ThreadState)
agent_state.add_node("agent", call_model_with_messages)
agent_state.add_edge(START, "agent")
agent_state.add_edge("agent", END)
graph = agent_state.compile(checkpointer=memory)   # Enables persistence

```

The `compile(checkpointer=memory)` call configures the graph to automatically serialize the current state to SQLite after each node execution, storing the complete `ThreadState` including the accumulated messages.

## Running and Resuming Conversations

When invoking the graph, you pass a `RunnableConfig` containing a unique `checkpoint_id`. LangGraph uses this identifier to load the previous state from SQLite, inject the saved message history, and execute the next step.

```python

# Example usage pattern

from open_notebook.graphs.chat import graph
from langchain_core.messages import HumanMessage

# First invocation creates the checkpoint

config = {"configurable": {"checkpoint_id": "chat-abc-123", "model_id": "gpt-4"}}
result = graph.invoke(
    {"messages": [HumanMessage(content="Hello")], "notebook": None},
    config=config
)

# Subsequent calls with the same checkpoint_id automatically restore history

config = {"configurable": {"checkpoint_id": "chat-abc-123", "model_id": "gpt-4"}}
continuation = graph.invoke(
    {"messages": [HumanMessage(content="Tell me more")], "notebook": None},
    config=config
)

```

The `SqliteSaver` writes the updated state back to `checkpoints.sqlite` under the same identifier, ensuring that `continuation["messages"]` contains the full conversation history including the new exchange.

## Handling Concurrent Access

The SQLite connection is initialized with `check_same_thread=False` to accommodate asyncio-based servers like FastAPI. This setting allows the same connection object to be used across different threads, which is safe because `SqliteSaver` handles synchronization internally. The checkpoints table stores each state as a JSON blob keyed by `checkpoint_id`, with individual rows containing the serialized message list and other state fields.

## Summary

- **Open Notebook** stores LangGraph checkpoints in `data/sqlite-db/checkpoints.sqlite` as defined in [`open_notebook/config.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/config.py).
- **SqliteSaver** instances are created in [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py) and [`source_chat.py`](https://github.com/lfnovo/open-notebook/blob/main/source_chat.py) using connections configured with `check_same_thread=False` for concurrent access.
- **ThreadState** uses `Annotated[list, add_messages]` to ensure message lists are merged rather than overwritten during state updates.
- **Graph compilation** with `checkpointer=memory` enables automatic persistence of the full state after each execution step.
- **Conversation continuity** is achieved by passing consistent `checkpoint_id` values in the `RunnableConfig`, allowing LangGraph to retrieve and update the stored message history across separate API calls.

## Frequently Asked Questions

### Where are chat histories physically stored in Open Notebook?

Chat histories are stored in a SQLite database file located at `./data/sqlite-db/checkpoints.sqlite` relative to the application root. This path is defined in [`open_notebook/config.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/config.py) through the `LANGGRAPH_CHECKPOINT_FILE` constant, which is imported by graph implementations to ensure a centralized storage location.

### How does LangGraph determine which conversation to restore?

LangGraph identifies conversations using the `checkpoint_id` parameter passed within the `configurable` dictionary of a `RunnableConfig`. When you invoke the graph with a specific `checkpoint_id`, the `SqliteSaver` loads the corresponding row from the SQLite `checkpoints` table and injects the previously saved state—including the `messages` list—into the graph's execution context.

### Can multiple users access the SQLite database simultaneously?

Yes, the implementation supports concurrent access because the SQLite connection is created with `check_same_thread=False`. This configuration is essential for FastAPI applications where multiple requests may access the database from different threads. The `SqliteSaver` manages internal synchronization, allowing safe shared access to the checkpoint file across concurrent API calls.

### What format are the messages stored in?

Messages are serialized as JSON within the SQLite database. The `SqliteSaver` serializes the entire `ThreadState` dictionary—including the list of LangChain `Message` objects—into a JSON format stored in the `checkpoints` table. When the conversation resumes, this JSON is deserialized back into Python objects, preserving the complete conversation structure and metadata.