88 lines
5.1 KiB
Text
88 lines
5.1 KiB
Text
|
|
let d = import "adr-defaults.ncl" in
|
||
|
|
|
||
|
|
d.make_adr {
|
||
|
|
id = "adr-004",
|
||
|
|
title = "SurrealDB as the Sole Persistence Layer",
|
||
|
|
status = 'Accepted,
|
||
|
|
date = "2024-11-01",
|
||
|
|
|
||
|
|
context = "Vapora requires relational storage (projects, tasks), graph traversal (agent relationships, knowledge graph), and document storage (execution history, LLM outputs) — typically requiring three separate databases. As of 2026-03-27, surrealdb v3 is in use (the markdown ADR references 2.3, which is stale). The workspace root Cargo.toml pins `surrealdb = { version = \"3\", features = [\"protocol-ws\", \"rustls\"] }`.",
|
||
|
|
|
||
|
|
decision = "SurrealDB is the only database engine in vapora. No PostgreSQL, no SQLite, no MongoDB, no Redis. All persistence goes through the SurrealDB client. Multi-tenancy is enforced via SurrealDB scopes — no application-layer tenant filtering may substitute for scope enforcement.",
|
||
|
|
|
||
|
|
rationale = [
|
||
|
|
{
|
||
|
|
claim = "Single database eliminates cross-DB transaction complexity",
|
||
|
|
detail = "If knowledge graph nodes and project tasks were in separate databases, any operation touching both (e.g. recording which task produced which KG node) would require distributed transactions or eventual consistency. SurrealDB handles both in one query.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
claim = "SurrealDB scopes provide database-level tenant isolation",
|
||
|
|
detail = "A query executed in scope workspace:X cannot access records in workspace:Y, regardless of application code. This means a bug in the service layer cannot cause a tenant data leak — the database rejects the query.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
claim = "SurrealQL graph traversal replaces a separate graph database",
|
||
|
|
detail = "Knowledge graph learning curves, agent relationship traversal, and causal execution chains are expressed as SurrealQL graph queries (->relation->). A separate Neo4j instance would require replication, synchronization, and schema management across two stores.",
|
||
|
|
},
|
||
|
|
],
|
||
|
|
|
||
|
|
consequences = {
|
||
|
|
positive = [
|
||
|
|
"Knowledge graph, task management, and audit trail share a single connection pool",
|
||
|
|
"SurrealDB scope enforcement is the primary multi-tenancy guarantee",
|
||
|
|
"All services use parameterized SurrealQL queries — no raw string interpolation into queries",
|
||
|
|
"WebSocket protocol-ws enables real-time subscriptions from vapora-backend",
|
||
|
|
],
|
||
|
|
negative = [
|
||
|
|
"SurrealDB v3 is a major-version breaking change from v2 — all services must coordinate upgrade simultaneously",
|
||
|
|
"SurrealDB lacks mature migration tooling compared to PostgreSQL — migrations are manual .surql files",
|
||
|
|
"No read replica support in current deployment (single instance handles all reads and writes)",
|
||
|
|
],
|
||
|
|
},
|
||
|
|
|
||
|
|
alternatives_considered = [
|
||
|
|
{
|
||
|
|
option = "PostgreSQL + Neo4j",
|
||
|
|
why_rejected = "Two database engines double operational burden. Cross-DB transactions require two-phase commit or saga patterns. Schema synchronization across both stores is error-prone.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
option = "MongoDB",
|
||
|
|
why_rejected = "No native graph traversal. Application code would need to implement graph traversal, duplicating logic that SurrealQL expresses natively. No built-in multi-tenancy scopes.",
|
||
|
|
},
|
||
|
|
],
|
||
|
|
|
||
|
|
constraints = [
|
||
|
|
{
|
||
|
|
id = "no-other-database-engines",
|
||
|
|
claim = "No crate in the workspace may import postgresql, mongodb, sqlite, or redis client crates",
|
||
|
|
scope = "vapora (all crates)",
|
||
|
|
severity = 'Hard,
|
||
|
|
check = { tag = 'Cargo, crate = "vapora-backend", forbidden_deps = ["sqlx", "sea-orm", "diesel", "mongodb", "redis"] },
|
||
|
|
rationale = "Adding a second database engine introduces consistency gaps, split connection pools, and dual migration paths.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id = "surreal-scopes-for-tenancy",
|
||
|
|
claim = "All multi-tenant queries must use SurrealDB scopes — no application-layer tenant_id filtering may be the sole isolation mechanism",
|
||
|
|
scope = "vapora-backend/src/services/",
|
||
|
|
severity = 'Hard,
|
||
|
|
check = { tag = 'Grep, pattern = "scope|NS|DB", paths = ["crates/vapora-backend/src/services/"], must_be_empty = false },
|
||
|
|
rationale = "Application-layer filtering is the second defense layer, not the primary one. A service bug that drops a WHERE clause cannot bypass the DB scope.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id = "parameterized-queries-only",
|
||
|
|
claim = "All SurrealQL queries must use parameterized bindings via .bind() — no string interpolation into query text",
|
||
|
|
scope = "vapora (all crates using surrealdb)",
|
||
|
|
severity = 'Hard,
|
||
|
|
check = { tag = 'Grep, pattern = "format!.*SELECT|INSERT|UPDATE|DELETE", paths = ["crates/"], must_be_empty = true },
|
||
|
|
rationale = "String-interpolated queries are vulnerable to SurrealQL injection, especially when tenant IDs or user-supplied values appear in query conditions.",
|
||
|
|
},
|
||
|
|
],
|
||
|
|
|
||
|
|
related_adrs = ["adr-002", "adr-011"],
|
||
|
|
|
||
|
|
ontology_check = {
|
||
|
|
decision_string = "surrealdb v3 is the sole database; scopes enforce multi-tenancy at DB level; no other DB engines; parameterized queries only",
|
||
|
|
invariants_at_risk = ["surreal-persistence", "multi-tenant-isolation"],
|
||
|
|
verdict = 'Safe,
|
||
|
|
},
|
||
|
|
}
|