Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

RFD-0020 — Per-tenant kernel runtime — shared base + per-tenant overlay

Committed Opened 2026-05-03 · Committed 2026-05-03

Question

The kernel serves many tenants concurrently. Should each tenant get an isolated kernel instance with its own copy of every loaded ontology, should everything share one global state with tenant-as-filter, or is there a hybrid?

Decision

Per-tenant kernel runtime: shared immutable base + per-tenant overlay.

  • A shared immutable base (UFO + Core + std) lives once under a reserved SHARED_BASE_TENANT_ID (UUID nil). Every tenant reads from this base; nobody writes to it.
  • Each tenant has a per-tenant overlay that adds tenant-specific concepts, roles, properties, rules, and compute registrations. The overlay is mutable per-tenant; cross-tenant writes are impossible by construction.
  • LayeredSchemaView resolves overlay-first with base fallback in a single SQL predicate: WHERE tenant_id IN (SHARED_BASE_TENANT_ID, $t).
  • Per-tenant Kernel instances exist as Arc<RwLock<BTreeMap<TenantId, Arc<RwLock<Kernel>>>>> in KernelState. Lazy load from Postgres + soft-cap eviction.
  • Tenant context propagates through the API layer via X-Tenant-Id header; TenantContextV2 extractor binds tenant scope per request.
  • Cross-tenant isolation is enforced at multiple layers: type-level (&mut self on Kernel carries tenant_id), runtime (every SQL scoped WHERE tenant_id IN (...)), integration-test verified.
  • Per-tenant domain registration via static Rust linking + per-tenant overlay registry. Form 2 / Form 3 computes (RFD-0006) live in the per-tenant overlay; name collisions impossible by construction.
  • Standpoints are tenant-local; primary standpoint sourced from tenant config.

Rationale

Shared base means UFO loads once. A naive isolated-instance design would have re-loaded every foundational ontology for every tenant — gigabytes of duplicated state, unbounded Postgres pressure. The shared base is the same content, read from one canonical location, with the SQL machinery making the read transparent.

Per-tenant overlay means tenant data stays tenant-local. Tenant A’s tax law amendments must not be visible to tenant B. The overlay scoping enforces this by default; cross-tenant reads require explicit join logic that has nowhere to live in normal code paths.

One-SQL read via reserved tenant ID. The shared base is queryable through the same SQL the per-tenant query uses, just with a wider IN predicate. No conditional logic in the read path; no “if shared else tenant” branching that would have produced the kind of bug we just paid an audit to find.

Type-level tenant carrying. Kernel instances carry their tenant ID in the type. A function that wants to operate on Kernel data must take &mut Kernel, which means it has a specific tenant in scope. There’s no API that lets you accidentally operate cross-tenant.

Consequences

  • KernelState (kernel/api/src/state.rs) maintains the per-tenant Kernel map with lazy load + soft-cap eviction.
  • All v2 API endpoints route through TenantContextV2. Write paths call require_principal(); missing principal surfaces as OE6005 PrincipalRequired.
  • Domain registration via static Rust linking. Per-tenant overlay registers in RustComputeRegistry.
  • Kernel Kripke commitments (RFD-0018’s modal operators) are preserved under per-tenant overlay; tenant-A and tenant-B see consistent modal evaluation against their respective overlays.
  • Cross-tenant queries are not a feature. If a future use case requires cross-tenant data, it would need an explicit RFD authorizing the boundary crossing.