How SurrealDB Handles Automatic Schema Migrations on API Startup in Open Notebook
Open Notebook automatically executes SurrealDB schema migrations during FastAPI startup using an AsyncMigrationManager that reads the current version from a _sbl_migrations table, compares it against numbered SurrealQL scripts in the migrations directory, and runs pending upgrades sequentially before the application accepts requests.
The lfnovo/open-notebook repository implements a fully asynchronous migration system that ensures the SurrealDB database schema stays synchronized with application code. By hooking into the FastAPI lifespan coroutine defined in api/main.py, the system guarantees schema consistency every time the API service launches, preventing runtime errors caused by outdated database structures.
Core Migration Architecture
The migration system centers on three primary components working together during the startup sequence:
AsyncMigrationManager– Orchestrates the detection and execution logic- Numbered SurrealQL files – Store schema changes in
open_notebook/database/migrations/ _sbl_migrationstable – Tracks the current schema version in SurrealDB
According to the source code in open_notebook/database/async_migrate.py, the manager loads migration scripts automatically and determines which ones need execution based on version comparison logic.
Step-by-Step Migration Process
When the API starts, the lifespan coroutine triggers a three-phase migration workflow:
Step 1: Detect Current Schema Version
The get_latest_version() function queries the hidden _sbl_migrations table via repo_query to determine the database's current state. As implemented in open_notebook/database/async_migrate.py:199‑205, the function returns the stored version number or defaults to 0 if the table does not exist yet.
from open_notebook.database.async_migrate import get_latest_version
current = await get_latest_version()
print(f"Current DB version: {current}") # Returns 0 on fresh database
Step 2: Determine Migration Requirements
The needs_migration() method compares the detected version against the number of available migration scripts loaded into self.up_migrations. If the stored version is less than the total available migrations, the system identifies which scripts require execution.
# Internally implemented at async_migrate.py:175‑179
if current_version < len(self.up_migrations):
return True # Migration required
Step 3: Execute Pending Migrations
The run_migration_up() method calls self.runner.run_all(), which iterates through pending migrations and executes each AsyncMigration.run() coroutine. Each migration:
- Opens a temporary SurrealDB connection using
db_connection()fromopen_notebook/database/repository.py:47‑62 - Executes the raw SurrealQL via
await connection.query(self.sql) - Calls
bump_version()to atomically update the version tracking table upon success
This sequential execution ensures schema changes apply in order, with the version bumping only occurring after successful query completion.
Migration File Structure and Discovery
Migration files reside in open_notebook/database/migrations/ and follow a strict naming convention:
- Up migrations:
1.surrealql,2.surrealql,3.surrealql(sequential integers) - Down migrations (optional):
1_down.surrealql,2_down.surrealql
The AsyncMigration.from_file() class method (lines 22‑35 in async_migrate.py) reads these files, strips comments, and stores the cleaned SQL for execution. Adding a new migration requires no code changes—simply create the next numbered file:
-- open_notebook/database/migrations/15.surrealql
CREATE table podcast_episode;
DEFINE FIELD title ON podcast_episode TYPE string;
FastAPI Lifespan Integration
The migration trigger lives in the lifespan coroutine within api/main.py. During startup (lines 178‑190), the code:
- Instantiates
AsyncMigrationManager - Calls
await manager.get_latest_version()to query the current state - Checks
await manager.needs_migration()to determine if work is required - Executes
await manager.run_migration_up()if migrations are pending - Logs the final version or "already up-to-date" status
If any migration fails, the system raises a RuntimeError that aborts API startup, ensuring the service never runs against an incompatible schema.
Error Handling and Safety Guarantees
The migration system implements strict safety measures:
- Atomic version tracking: The
bump_version()function updates_sbl_migrationsonly after successful query execution - Startup blocking: Failures in
run_migration_up()propagate to the lifespan coroutine, preventing the FastAPI application from initializing - Connection isolation: Each migration uses a fresh connection via
db_connection(), preventing connection state pollution between migrations
Practical Usage Examples
Running Migrations Manually
You can trigger migrations outside of the standard startup flow using the manager directly:
from open_notebook.database.async_migrate import AsyncMigrationManager
async def migrate():
manager = AsyncMigrationManager()
if await manager.needs_migration():
await manager.run_migration_up()
print("Migrations applied successfully")
else:
print("Database already at latest version")
Adding a New Schema Change
- Create
open_notebook/database/migrations/16.surrealql:
-- Add indexes for performance
DEFINE INDEX idx_note_created ON note FIELDS created_at;
- Optionally create
16_down.surrealqlfor rollback support - Restart the API—the
AsyncMigrationManagerautomatically detects and executes the new script
Summary
- Automatic detection: The
AsyncMigrationManagerqueries the_sbl_migrationstable on startup to determine current schema version - Sequential execution: Migrations run in order from
1.surrealqlthrough the latest numbered file, with each query executing in its own database connection - Zero-downtime safety: The API aborts startup if migrations fail, preventing code/schema mismatches
- File-based management: Simply add numbered SurrealQL files to
open_notebook/database/migrations/without modifying Python code - Async-first design: All migration logic uses
async/awaitpatterns compatible with FastAPI's asynchronous architecture
Frequently Asked Questions
What happens if a migration fails during startup?
The system raises a RuntimeError in the lifespan coroutine at api/main.py, which prevents the FastAPI application from completing startup. This ensures the service never runs with a partially applied or outdated schema. You must fix the failing SurrealQL syntax or database issue before the API can start.
How do I add a new schema migration to Open Notebook?
Create a new SurrealQL file in open_notebook/database/migrations/ using the next sequential number (e.g., 17.surrealql). Write your schema changes using standard SurrealQL syntax. Optionally create a matching 17_down.surrealql file for rollback support. The AsyncMigrationManager automatically detects new files on the next API startup.
Where does SurrealDB store the current migration version?
Open Notebook tracks versions in a hidden table named _sbl_migrations within your SurrealDB database. The get_latest_version() function queries this table via repo_query, and bump_version() updates it after each successful migration. If the table is missing, the system assumes version 0 and creates the table during the first migration run.
Can I run migrations manually outside of the API startup?
Yes. Import AsyncMigrationManager from open_notebook.database.async_migrate and call await manager.run_migration_up() within an async context. This uses the same code path as the automatic startup sequence, querying the current version and executing any pending migrations from the open_notebook/database/migrations/ directory.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →