Some checks failed
Documentation Lint & Validation / Markdown Linting (push) Has been cancelled
Documentation Lint & Validation / Validate mdBook Configuration (push) Has been cancelled
Documentation Lint & Validation / Content & Structure Validation (push) Has been cancelled
mdBook Build & Deploy / Build mdBook (push) Has been cancelled
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
Documentation Lint & Validation / Lint & Validation Summary (push) Has been cancelled
mdBook Build & Deploy / Documentation Quality Check (push) Has been cancelled
mdBook Build & Deploy / Deploy to GitHub Pages (push) Has been cancelled
mdBook Build & Deploy / Notification (push) Has been cancelled
on+re:
- core.ncl: 5 new Practice nodes (notification-channels,
vapora-capabilities, agent-hot-reload-stable-identity,
merkle-audit-trail, notification-channels) + 5 new edges;
knowledge-graph-execution-history updated with HNSW+BM25+RRF
- state.ncl: production-readiness blocker/catalyst updated (hot-reload
complete, BudgetManager/LLMRouter still require restart);
ontoref-integration catalyst updated (vapora-ontology/reflection
crates, api-catalog.json, nickel contracts)
ADRs (NCL):
- adr-013: KG hybrid search — HNSW+BM25+RRF, rejected in-process scan
- adr-014: capability packages — AgentDefinition→vapora-shared,
DashMap shard-before-await constraint
- adr-015: Merkle audit trail — SHA-256 hash chain, rejected HMAC
- adr-016: agent hot-reload — stable_id=role, learning_profiles survive
drain, BudgetManager excluded from reload scope
landing page:
- 2 new feature boxes: VCS-Agnostic Worktree (jj/git), Ontology Protocol
- KG box: 20→28 tests, HNSW+BM25+RRF description
- Agents box: 71→82 tests, hot-reload + stable_id
- tech stack: Rust 21→23 crates, added jj, Radicle, ontoref badges
- status badge: 620→691 tests
85 lines
5.3 KiB
XML
85 lines
5.3 KiB
XML
let d = import "adr-defaults.ncl" in
|
||
|
||
d.make_adr {
|
||
id = "adr-010",
|
||
title = "SurrealDB Scope-Based Multi-Tenancy with Application-Layer Defense-in-Depth",
|
||
status = 'Accepted,
|
||
date = "2024-11-01",
|
||
|
||
context = "Vapora serves multiple workspaces (tenants) from a single backend instance. Tenant isolation must guarantee that workspace A cannot read or write workspace B's data. The isolation mechanism must be enforced at a layer that application code bugs cannot bypass. SurrealDB scopes provide database-level isolation; application-layer tenant_id validation provides the defense-in-depth second layer.",
|
||
|
||
decision = "Multi-tenancy is enforced at two layers: (1) SurrealDB scopes — all database connections and queries execute within a scope tied to the workspace; (2) Application services validate tenant_id in every write and read query as a redundant check. The SurrealDB scope is the primary isolation guarantee. Application-layer filtering is defense-in-depth only — it must never be the sole isolation mechanism.",
|
||
|
||
rationale = [
|
||
{
|
||
claim = "Database-level scope enforcement cannot be bypassed by application code bugs",
|
||
detail = "A service layer bug that omits a WHERE tenant_id = ? clause will still fail to return another tenant's data if the connection is scoped. The scope check runs in the database before the query result is assembled.",
|
||
},
|
||
{
|
||
claim = "Application-layer validation catches bugs before they reach the database",
|
||
detail = "If the SurrealDB scope configuration has an error, application-layer tenant_id checks provide a second line of defense. Defense-in-depth means no single failure mode causes a tenant data leak.",
|
||
},
|
||
{
|
||
claim = "SurrealDB scopes are the most cost-effective isolation for a single-instance deployment",
|
||
detail = "Hard partitioning (separate database per tenant) would require N database connections, N migration runs, and N monitoring streams. Scopes achieve logical isolation at the query level without multiplying operational burden.",
|
||
},
|
||
],
|
||
|
||
consequences = {
|
||
positive = [
|
||
"Cross-tenant data leaks require both a scope misconfiguration AND an application bug simultaneously",
|
||
"Tenant onboarding is a scope creation operation — no schema migration or new instance needed",
|
||
"All SurrealQL queries are tenant-scoped by default — queries that omit tenant context fail, not return all data",
|
||
],
|
||
negative = [
|
||
"Cross-tenant analytics queries (aggregate usage across all workspaces) require a superuser scope connection — this is a privileged operation that must be explicitly controlled",
|
||
"SurrealDB scope token expiry handling must be implemented to prevent session leaks between tenant requests",
|
||
],
|
||
},
|
||
|
||
alternatives_considered = [
|
||
{
|
||
option = "Application-layer tenant_id filtering only",
|
||
why_rejected = "A single WHERE clause omission leaks all tenants' data. Application code bugs are more common than database configuration errors.",
|
||
},
|
||
{
|
||
option = "Separate database instance per tenant",
|
||
why_rejected = "N databases × all maintenance operations. Initial vapora deployment targets small-to-medium teams; the operational overhead of per-tenant instances is disproportionate.",
|
||
},
|
||
],
|
||
|
||
constraints = [
|
||
{
|
||
id = "no-application-only-isolation",
|
||
claim = "No service may rely solely on application-layer tenant_id filtering for tenant isolation — SurrealDB scope must be the primary mechanism",
|
||
scope = "vapora-backend/src/services/",
|
||
severity = 'Hard,
|
||
check = { tag = 'Grep, pattern = "\\.signin|scope|Scope", paths = ["crates/vapora-backend/src/services/"], must_be_empty = false },
|
||
rationale = "Application-layer-only filtering has caused multi-tenant data leaks in production systems. Database scope enforcement is not optional.",
|
||
},
|
||
{
|
||
id = "tenant-id-in-all-writes",
|
||
claim = "Every INSERT and UPDATE in service layer must include tenant_id binding",
|
||
scope = "vapora-backend/src/services/",
|
||
severity = 'Hard,
|
||
check = { tag = 'Grep, pattern = "tenant_id", paths = ["crates/vapora-backend/src/services/"], must_be_empty = false },
|
||
rationale = "Defense-in-depth requires the application layer to enforce tenant context independently of the scope mechanism.",
|
||
},
|
||
{
|
||
id = "no-cross-tenant-queries-without-superuser",
|
||
claim = "Queries that aggregate across tenant boundaries must use an explicitly designated superuser scope — not the tenant session",
|
||
scope = "vapora-backend/src/services/, vapora-analytics/",
|
||
severity = 'Hard,
|
||
check = { tag = 'Grep, pattern = "superuser|admin_scope|root", paths = ["crates/vapora-analytics/"], must_be_empty = false },
|
||
rationale = "A routine service accidentally executing with a superuser scope would leak data from all tenants in its result set.",
|
||
},
|
||
],
|
||
|
||
related_adrs = ["adr-004"],
|
||
|
||
ontology_check = {
|
||
decision_string = "SurrealDB scopes as primary tenant isolation; application tenant_id validation as defense-in-depth; no application-only isolation; no cross-tenant queries without superuser scope",
|
||
invariants_at_risk = ["multi-tenant-isolation", "surreal-persistence"],
|
||
verdict = 'Safe,
|
||
},
|
||
}
|