# ADR-006: SurrealDB 3.0 Engine Abstraction **Status**: Accepted **Date**: 2026-02-21 **Deciders**: Architecture Team **Supersedes**: [ADR-003: Hybrid Storage Strategy](003-hybrid-storage.md) (SurrealDB connection model) --- ## Context SurrealDB 2.x required a concrete connection type at compile time (`Surreal`, `Surreal`, etc.), forcing a single engine choice per binary. This created three problems: 1. **No runtime engine selection**: switching from embedded to remote required recompilation 2. **Tight test coupling**: tests depended on whichever engine was compiled in 3. **No dual-layout support**: using SurrealKV for graph data and RocksDB for hot data required separate crates or unsafe casting SurrealDB 3.0 introduces `surrealdb::engine::any::connect(url)` returning `Surreal` — a type-erased connection dispatched at runtime by URL scheme: | URL scheme | Engine | Characteristics | |---|---|---| | `mem://` | In-memory | Ephemeral, test isolation | | `surrealkv://path` | SurrealKV (B-tree) | Embedded, relational/graph data | | `rocksdb://path` | RocksDB (LSM) | Embedded, append-heavy hot data | | `ws://host:port` | WebSocket | Remote, team/shared deployment | This makes the engine a pure config concern. --- ## Decision **Use `Surreal` throughout `SurrealDbStorage`, selecting the engine from `SurrealEngineConfig` at runtime via URL dispatch.** ### Config Schema `SurrealEngineConfig` is a serde-tagged enum serialized with `tag = "engine"`: ```rust #[serde(tag = "engine", rename_all = "snake_case")] pub enum SurrealEngineConfig { Mem, SurrealKv { path: String }, RocksDb { path: String }, Ws { url: String }, } ``` `to_url()` maps variants to the URL scheme `engine::any::connect` expects: ```rust impl SurrealEngineConfig { pub fn to_url(&self) -> String { match self { Self::Mem => "mem://".to_string(), Self::SurrealKv { path } => format!("surrealkv://{path}"), Self::RocksDb { path } => format!("rocksdb://{path}"), Self::Ws { url } => url.clone(), } } } ``` ### Dual Hot/Cold Layout `SurrealDbStorage` holds two independent `Surreal` connections: ```rust pub struct SurrealDbStorage { graph_db: Surreal, // SurrealKV default — nodes, edges, metadata hot_db: Surreal, // RocksDB default — embeddings, session logs namespace: String, } ``` Default production layout: ```nickel secondary.surrealdb = { graph = { engine = "surreal_kv", path = ".kogral/db/graph" }, hot = { engine = "rocks_db", path = ".kogral/db/hot" }, namespace = "kogral", } ``` Test layout (in-memory, no filesystem side effects): ```nickel secondary.surrealdb = { graph = { engine = "mem" }, hot = { engine = "mem" }, namespace = "test", } ``` Remote team deployment (single SurrealDB instance, two databases): ```nickel secondary.surrealdb = { graph = { engine = "ws", url = "ws://kb.company.com:8000" }, hot = { engine = "ws", url = "ws://kb.company.com:8000" }, namespace = "engineering", } ``` ### Storage Factory `storage::factory::build()` performs the runtime dispatch, keeping SurrealDB-specific logic behind the `surrealdb-backend` feature gate: ```rust #[allow(clippy::unused_async)] pub async fn build(config: &StorageConfig, base_path: PathBuf) -> Result> { #[cfg(feature = "surrealdb-backend")] if config.secondary.enabled && config.secondary.storage_type == SecondaryStorageType::Surrealdb { let db = SurrealDbStorage::from_config(&config.secondary.surrealdb).await?; return Ok(Box::new(db)); } Ok(match config.primary { StorageType::Memory => Box::new(MemoryStorage::new()), StorageType::Filesystem => Box::new(FilesystemStorage::new(base_path)), }) } ``` ### Type Erasure Composition `impl Storage for Box` enables `EventingStorage>` without knowing the concrete backend at compile time: ```rust #[async_trait] impl Storage for Box { async fn save_graph(&mut self, graph: &Graph) -> Result<()> { (**self).save_graph(graph).await } // ... full delegation for all methods } ``` ### CRUD via `serde_json::Value` SurrealDB 3.0 removed the `IntoSurrealValue`/`SurrealValue` traits. All CRUD goes through `serde_json::Value` as the intermediary: ```rust // save_node let row = serde_json::to_value(node) .map_err(|e| KbError::Storage(e.to_string()))?; let _: Option = self.graph_db .upsert(("nodes", format!("{graph_name}__{}", node.id))) .content(row) .await .map_err(|e| KbError::Storage(e.to_string()))?; ``` `.bind()` parameters require `'static` values — `&str` arguments must be `.to_string()`: ```rust let nodes: Vec = self.graph_db .query("SELECT * FROM nodes WHERE project = $g") .bind(("g", graph_name.to_string())) // .to_string() required .await... ``` ### Error Handling `KbError::Database(#[from] surrealdb::Error)` was removed. All SurrealDB errors convert via `map_err(|e| KbError::Storage(e.to_string()))`, avoiding `#[from]` coupling to a feature-gated type that would break compilation on default features. --- ## Consequences ### Positive - Engine selection is a config value, not a compile-time decision - Tests run fully in-memory (`engine = "mem"`) with zero filesystem side effects - Dual layout (SurrealKV + RocksDB) is the embedded production default - Remote deployment (WebSocket) requires only a config change - `Storage` trait consumers never see SurrealDB types — `Box` is the boundary ### Negative - `Surreal` has slightly higher dispatch overhead than concrete types (negligible vs. I/O) - `serde_json::Value` intermediary adds one extra allocation per CRUD call - `engine::any` requires all four engine feature flags compiled in when `surrealdb-backend` is enabled (larger binary) --- ## Feature Matrix ```toml [features] default = ["filesystem"] filesystem = [] surrealdb-backend = ["dep:surrealdb"] nats-events = ["dep:platform-nats", "dep:bytes"] orchestration = ["nats-events", "dep:stratum-orchestrator"] full = ["surrealdb-backend", "nats-events", "orchestration"] ``` SurrealDB dependency activates all four engines: ```toml surrealdb = { workspace = true, optional = true, features = ["kv-surrealkv", "kv-rocksdb", "protocol-ws", "rustls"] } ``` --- ## References - [SurrealDB `engine::any` docs](https://surrealdb.com/docs/sdk/rust/setup) - [SurrealDbStorage](../../../crates/kogral-core/src/storage/surrealdb.rs) - [Storage Factory](../../../crates/kogral-core/src/storage/factory.rs) - [Config Schema](../../../crates/kogral-core/src/config/schema.rs) - [Nickel Defaults](../../../schemas/kogral/defaults.ncl) --- ## Revision History | Date | Author | Change | |---|---|---| | 2026-02-21 | Architecture Team | Initial decision — SurrealDB 3.0 + engine::any | --- **Previous ADR**: [ADR-005: MCP Protocol](005-mcp-protocol.md) **Next ADR**: [ADR-007: NATS Event Publishing](007-nats-event-publishing.md)