Key changes: new events.rs (NATS EventingStorage decorator), storage/factory.rs (backend selection), orchestration.rs, SurrealDB v3 engine upgrade, expanded Nickel schemas, and two new ADRs (006, 007).
5.3 KiB
SurrealDB Storage
KOGRAL uses SurrealDB 3.0 as its scalable backend, enabled via the surrealdb-backend Cargo feature.
The integration is built on surrealdb::engine::any::connect(url), which selects the engine at
runtime from a URL scheme — no recompilation required when switching between embedded, in-memory,
or remote deployments.
Dual Hot/Cold Layout
SurrealDbStorage maintains two independent database connections:
| Connection | Default engine | URL | Purpose |
|---|---|---|---|
graph_db |
SurrealKV (B-tree) | surrealkv://.kogral/db/graph |
Nodes, edges, graph metadata |
hot_db |
RocksDB (LSM) | rocksdb://.kogral/db/hot |
Embeddings, session logs, append data |
SurrealKV's B-tree layout favours point lookups and range scans (node/graph queries). RocksDB's LSM tree favours sequential writes (embedding vectors, event logs). Separating them avoids write-amplification cross-contamination.
Supported Engines
All four engines are compiled in when the surrealdb-backend feature is active:
Nickel engine |
URL scheme | Cargo feature | Use case |
|---|---|---|---|
mem |
mem:// |
kv-mem |
Tests, ephemeral dev sessions |
surreal_kv |
surrealkv://path |
kv-surrealkv |
Embedded production (default graph) |
rocks_db |
rocksdb://path |
kv-rocksdb |
Embedded production (default hot) |
ws |
ws://host:port |
protocol-ws |
Remote team / shared deployments |
Configuration
Embedded (default production)
storage = {
primary = 'filesystem,
secondary = {
enabled = true,
type = 'surrealdb,
surrealdb = {
graph = { engine = "surreal_kv", path = ".kogral/db/graph" },
hot = { engine = "rocks_db", path = ".kogral/db/hot" },
namespace = "kogral",
},
},
}
In-Memory (tests, CI)
storage = {
primary = 'memory,
secondary = {
enabled = true,
type = 'surrealdb,
surrealdb = {
graph = { engine = "mem" },
hot = { engine = "mem" },
namespace = "test",
},
},
}
Remote WebSocket (team/shared deployment)
storage = {
primary = 'filesystem,
secondary = {
enabled = true,
type = 'surrealdb,
surrealdb = {
graph = { engine = "ws", url = "ws://kb.company.com:8000" },
hot = { engine = "ws", url = "ws://kb.company.com:8000" },
namespace = "engineering",
},
},
}
Building with SurrealDB Support
# Debug build
cargo build -p kogral-core --features surrealdb-backend
# All features (SurrealDB + NATS + orchestration)
cargo build -p kogral-core --all-features
# Justfile shortcut
just build::core-db
CRUD Pattern
All CRUD operations route through serde_json::Value as the intermediary type (SurrealDB 3.0
removed IntoSurrealValue/SurrealValue). The key format for nodes is
("{graph_name}__{node_id}") on the nodes table:
// upsert
let row = serde_json::to_value(node)?;
let _: Option<serde_json::Value> = graph_db
.upsert(("nodes", format!("{graph_name}__{}", node.id)))
.content(row)
.await?;
// select
let raw: Option<serde_json::Value> = graph_db
.select(("nodes", format!("{graph_name}__{node_id}")))
.await?;
// delete
let _: Option<serde_json::Value> = graph_db
.delete(("nodes", format!("{graph_name}__{node_id}")))
.await?;
// list by graph (query API)
let nodes: Vec<Node> = graph_db
.query("SELECT * FROM nodes WHERE project = $g")
.bind(("g", graph_name.to_string()))
.await?
.take(0)?;
.bind() parameters require owned String values — &str slices do not satisfy the 'static
bound in SurrealDB 3.0's bind API.
Hot Data Methods
SurrealDbStorage exposes direct methods on hot_db that are outside the Storage trait:
// Store embedding vector for a node
pub async fn save_embedding(&self, node_id: &str, vector: &[f32]) -> Result<()>
// Append a session event to the log
pub async fn log_session(&self, entry: &serde_json::Value) -> Result<()>
These operate on the embeddings and sessions tables in hot_db.
NATS Event Integration
When the nats-events feature is enabled and config.nats is present, the storage factory
wraps SurrealDbStorage (or any other backend) with EventingStorage. Every mutation emits
a NATS JetStream event:
kogral.<graph>.node.saved → NodeSaved { graph, node_id, node_type }
kogral.<graph>.node.deleted → NodeDeleted { graph, node_id }
kogral.<graph>.graph.saved → GraphSaved { name, node_count }
See ADR-007: NATS Event Publishing for design rationale.
Feature Matrix
| Feature | Includes |
|---|---|
filesystem (default) |
FilesystemStorage only |
surrealdb-backend |
SurrealDbStorage + all four engines |
nats-events |
EventingStorage, KogralEvent, NATS JetStream client |
orchestration |
nats-events + stratum-orchestrator bridge |
full |
All of the above |