Vapora/adrs/adr-010-multi-tenancy.ncl
Jesús Pérez 75e5ebd9a2
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
chore: ontology sync + 4 NCL ADRs + landing page update
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
2026-04-07 21:06:48 +01:00

85 lines
5.3 KiB
XML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
},
}