How Open Notebook's FastAPI Backend Implements a Three-Tier Architecture with SurrealDB

Open Notebook's FastAPI backend implements a three-tier architecture by using an async lifespan context for database migrations, a repository pattern for SurrealDB connections, and dedicated service layers that translate HTTP requests into SurrealQL queries.

The lfnovo/open-notebook repository demonstrates a production-ready separation of concerns across presentation, application, and data tiers. The FastAPI backend serves as the asynchronous application tier, orchestrating between the React frontend and SurrealDB's graph-oriented data store through WebSocket connections. This architecture ensures non-blocking I/O across all database operations while maintaining strict boundaries between HTTP handling, business logic, and data persistence.

Application Tier Initialization and Lifespan Management

The FastAPI application tier boots through a structured lifespan protocol defined in api/main.py. During startup (lines 98–104), the system initializes an AsyncMigrationManager that ensures the SurrealDB schema is current before handling any requests.

The lifespan context manager handles the complete initialization sequence:


# api/main.py (lines 98-104, 115-127)

@asynccontextmanager
async def lifespan(app: FastAPI):
    logger.info("Starting API initialization…")
    migration_manager = AsyncMigrationManager()
    if await migration_manager.needs_migration():
        await migration_manager.run_migration_up()
    logger.success("API initialization completed successfully")
    yield

This pattern ensures that the database schema in open_notebook/database/migrations/*.surrealql is applied consistently before the application accepts traffic. The AsyncMigrationManager implementation in open_notebook/database/async_migrate.py reads migration files, checks current versioning, and executes pending schema changes atomically.

After migration, the main application instance loads CORS middleware and registers routers (lines 57–89, 90–113), creating the HTTP-facing surface of the application tier.

Database Connection Management with Async Context Managers

The data tier connectivity is abstracted through a reusable async context manager in open_notebook/database/repository.py (lines 47–62, 65–83). This implementation opens a WebSocket connection to SurrealDB, authenticates the session, selects the appropriate namespace and database, and guarantees connection closure after each operation.


# open_notebook/database/repository.py (excerpt)

@asynccontextmanager
async def db_connection():
    async with AsyncSurreal(settings.SURREAL_DB_URL) as connection:
        await connection.signin({
            "user": settings.SURREAL_DB_USER,
            "pass": settings.SURREAL_DB_PASS,
        })
        await connection.use(settings.SURREAL_NS, settings.SURREAL_DB)
        yield connection

All CRUD operations consume this context manager, ensuring that database connections are never leaked and that each operation runs within a authenticated, isolated session. This design pattern effectively decouples the application tier from connection management details while maintaining async performance characteristics.

SurrealDB CRUD Operations and Data Translation

The repository layer in open_notebook/database/repository.py (lines 85–140, 150–195) provides thin wrappers around SurrealQL that handle type conversion, timestamp injection, and RecordID normalization. These functions translate between SurrealDB's graph-native responses and plain Python dictionaries consumed by the service layer.

Creating records with automatic timestamp handling:


# open_notebook/database/repository.py (excerpt)

async def repo_create(table: str, data: Dict[str, Any]) -> Dict[str, Any]:
    data.pop("id", None)  # Remove client-side IDs

    data["created"] = datetime.now(timezone.utc)
    data["updated"] = datetime.now(timezone.utc)
    async with db_connection() as connection:
        result = parse_record_ids(await connection.insert(table, data))
        return result

Querying records with SurrealQL injection protection:


# open_notebook/database/repository.py

async def repo_query(query_str: str, vars: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
    async with db_connection() as connection:
        result = parse_record_ids(await connection.query(query_str, vars))
        return result

The repo_upsert, repo_update, and repo_delete functions follow identical patterns, ensuring consistent error handling and type conversion across all data tier interactions. The parse_record_ids helper normalizes SurrealDB's complex record identifiers into serializable formats suitable for JSON responses.

Router Wiring and Service Layer Organization

The application tier implements clear separation between HTTP handling and business logic through distinct router and service modules. In api/main.py (lines 89–113), logical API groups are registered using app.include_router(), mounting endpoints under /api/* paths.

Example request flow through the three-tier stack:

  1. Presentation Tier: React frontend sends GET /api/notebooks/{notebook_id}
  2. Application Tier Router: api/routers/notebooks.py receives the request

# api/routers/notebooks.py

@router.get("/{notebook_id}", response_model=NotebookResponse)
async def get_notebook(notebook_id: str):
    return await notebook_service.get_notebook(notebook_id)
  1. Application Tier Service: api/notebook_service.py contains business logic

# api/notebook_service.py

async def get_notebook(notebook_id: str) -> dict:
    query = f"SELECT * FROM notebook:{notebook_id}"
    result = await repo_query(query)
    return result[0] if result else {}
  1. Data Tier: Repository executes SurrealQL via WebSocket and returns normalized data

This architecture ensures that route handlers remain thin, delegating validation and business rules to service modules while maintaining the repository as the sole data access point. Error handling and CORS management (lines 198–226 in api/main.py) ensure that exceptions return JSON with proper headers, keeping the frontend able to read error bodies across origins.

Summary

  • Lifespan Management: The FastAPI backend uses an async context manager in api/main.py to run database migrations via AsyncMigrationManager before accepting traffic, ensuring schema consistency.
  • Connection Management: The db_connection context manager in open_notebook/database/repository.py handles WebSocket authentication and cleanup, providing isolated SurrealDB sessions for every operation.
  • Data Translation: Repository helpers like repo_create and repo_query convert SurrealDB RecordID objects and timestamps into standard Python dictionaries, abstracting graph database complexity.
  • Layer Separation: Routers in api/routers/*.py delegate to service modules (e.g., api/notebook_service.py), which consume repository functions—maintaining strict boundaries between HTTP, business logic, and data persistence tiers.

Frequently Asked Questions

How does the FastAPI backend handle database migrations automatically?

The backend implements an AsyncMigrationManager class that runs during the FastAPI lifespan startup sequence. Located in open_notebook/database/async_migrate.py, this manager checks the current schema version against migration files in open_notebook/database/migrations/*.surrealql and applies pending changes before yielding control to the application. This ensures the SurrealDB schema is always synchronized with the application code before any HTTP requests are processed.

Why use SurrealDB instead of a traditional relational database with FastAPI?

According to the source code, SurrealDB serves as a graph-oriented data tier that stores notebooks, sources, notes, embeddings, and relationship graphs in a unified structure. The repository pattern in open_notebook/database/repository.py leverages SurrealDB's native WebSocket support for persistent connections, allowing asynchronous queries without the connection pooling overhead typical of PostgreSQL or MySQL. This aligns with FastAPI's async nature while supporting complex graph relationships between entities.

How does the three-tier architecture prevent database connection leaks?

The architecture implements a strict context manager pattern in open_notebook/database/repository.py (lines 47–62). Every database operation uses async with db_connection() as connection, which guarantees that the WebSocket connection to SurrealDB is authenticated, used for the specific operation, and closed—even if exceptions occur. This design ensures that the application tier never holds idle connections, preventing resource exhaustion under load.

What happens when a SurrealQL query fails in the repository layer?

Repository functions in open_notebook/database/repository.py wrap all SurrealDB calls in error handling that catches exceptions and converts them into application-tier errors. The repo_query and related functions normalize responses through parse_record_ids, ensuring that malformed data or connection failures return consistent Python structures rather than raw SurrealDB exceptions. This allows service layers to handle data logic without managing database-specific error states.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →