---
type: Axis
title: tenancy (isolation axis)
description: The tenancy/isolation architecture axis — how strongly separate tenants' data is isolated (single → shared-multi → schema-per-tenant → DB-per-tenant). A system-singleton axis whose commitment surface is the persistent-data schema. DEEP (RFC #75 5A.4 / P1 #851 → promoted 4.5.0a6 #856); worked example is the single-tenant-by-design posture (one project per canonical store, per-project daemon scope, frozen 3.5.0) that is in fact already DB-per-tenant if "project" is read as the tenant.
tags: [axis, architecture, tenancy, isolation, multi-tenant, rfc-75, deep]
timestamp: 2026-06-18T00:00:00Z
---

# Tenancy (isolation axis)

> **DEEP axis (P1 #851 → 4.5.0a6 #856).** Full §6 depth with a worked example
> from the skill's own history — the skill is single-tenant *within* a store (no
> `tenant_id` anywhere) but the per-project daemon design (one DB per project,
> frozen 3.5.0) means that if "project" is read as the tenant, the system is
> already at the *top* rung (physical DB-per-tenant) by a different route, never
> having passed through shared-multi. The worked example walks that subtlety and
> names the `tenant_id` drift signal.

## Concept

Tenancy is the **isolation** axis (RFC #75 §3): how strongly separate tenants'
data is kept apart. The ladder runs single (one tenant; no isolation concept) →
shared-multi (one schema, a `tenant_id` discriminator column on every row) →
schema-per-tenant (a logical schema per tenant in a shared DB) → DB-per-tenant
(a physical database per tenant). Its **commitment surface is the
persistent-data schema** (§4): the isolation mechanism is baked into where + how
rows live, and tightening it is a data migration. Its **scope is
system-singleton** (§5).

## Why it is an axis (discriminator test — §2)

1. **Ordered tiers** — each rung isolates tenants more strongly.
2. **Cross-cutting bundle** — a rung dictates the schema (discriminator columns
   vs separate schemas/DBs), every query's tenant scoping, the connection
   routing, and the cross-tenant-leak tests.
3. **Asymmetric migration cost** — moving from single to shared-multi means
   adding a `tenant_id` to every row + scoping every query; splitting to
   schema/DB-per-tenant is a full data relocation.
4. **Requirement-keyed selection** — falsifiable probes pick the rung.

## Schema view (the §6 entry — validated by `axis_catalog`)

```yaml
id: tenancy
kind: architecture-axis
scope: system-singleton
selection:
  probes:
    - "Is there exactly one tenant (a single-org/single-project tool)? → single."
    - "Must many tenants share infrastructure with logical separation? → shared-multi (tenant_id on every row)."
    - "Do tenants need stronger isolation (separate schemas, independent migration)? → schema-per-tenant."
    - "Do tenants need physical/regulatory isolation or independent backup/restore? → DB-per-tenant."
    - "Did a query forget its tenant scope (a cross-tenant leak)? → the isolation discipline of the current rung failed."
composes_with:
  - "tenant membership = principal × tenancy"
  - "per-tenant retention = tenancy × temporality"
applies_when: []
tiers:
  - tier: single
    commitment_delta:
      surface: persistent-data-schema
      delta: "one tenant; no tenant discriminator, no scoping. All data belongs to the single tenant."
    admin_tool: "none."
    enforcement:
      - "no cross-tenant concept exists; nothing to scope."
    tests:
      - "operations need no tenant context."
    forecloses: "serving more than one isolated tenant."
    migration_cost_to_next: "high — add tenant_id to every row + scope every query."
  - tier: shared-multi
    commitment_delta:
      surface: persistent-data-schema
      delta: "one shared schema; every row carries a tenant_id; every query is tenant-scoped. Isolation is logical, enforced by discipline."
    admin_tool: "tenant provisioning; a tenant-scope audit over queries."
    enforcement:
      - "every read/write filters by tenant_id; a query without a tenant scope is rejected."
    tests:
      - "tenant A cannot read tenant B's rows; an unscoped query is caught."
    forecloses: "independent per-tenant migration/backup (one schema for all)."
    migration_cost_to_next: "high — split into a logical schema per tenant."
  - tier: schema-per-tenant
    commitment_delta:
      surface: persistent-data-schema
      delta: "a separate logical schema per tenant within a shared DB; isolation is structural, migration can be per-tenant."
    admin_tool: "per-schema migration runner; schema provisioning."
    enforcement:
      - "connections resolve to the tenant's schema; no cross-schema query path exists."
    tests:
      - "a migration applies per tenant independently; cross-schema access is impossible."
    forecloses: "single-migration-for-all simplicity."
    migration_cost_to_next: "high — relocate each tenant to its own physical database."
  - tier: db-per-tenant
    commitment_delta:
      surface: persistent-data-schema
      delta: "a physical database per tenant; the strongest isolation (independent backup/restore, residency, blast radius)."
    admin_tool: "per-tenant DB provisioning + connection routing + backup."
    enforcement:
      - "tenant traffic routes to the tenant's DB; no shared store exists."
    tests:
      - "a tenant DB failure does not affect others; per-tenant restore works."
    forecloses: "shared-infrastructure economics; per-tenant operational cost."
    migration_cost_to_next: "n/a — top of the ladder."
examples:
  - "Read at the level of the canonical store, the skill is SINGLE-tenant: there is no `tenant_id` discriminator on any row, no tenant-scoping predicate on any query, and every row belongs implicitly to the one project. But the more interesting reading — and the one this axis's per-system selection surfaces — is that the per-project daemon design (frozen 3.5.0: one daemon, one DB, one project) is *physical isolation by project*, so if you take 'project' to be the tenant, the system already sits at the TOP rung (DB-per-tenant) without ever having built shared-multi: two projects can never leak into each other because they are different sqlite files behind different daemons, which is the strongest isolation, achieved structurally rather than by a discriminator-column discipline. That is why the cross-tenant-leak failure mode this axis warns about (a query forgetting its tenant scope) simply cannot occur here — there is no shared schema to forget to scope. The drift signal is therefore precise and load-bearing: if a `tenant_id` column ever appeared on a canonical row, it would mean the design had quietly collapsed DB-per-project into shared-multi-tenant-in-one-store — the multi-tenant-in-fact downstream-divergence signal (RFC #75 §13.5b) — and that is exactly the schema change the architecture_drift_gap detector watches for against a record that still declares single/per-project isolation."
```

## Selection procedure (§8)

Bidirectional check; chosen once. Recorded in the system architecture record; a
`tenant_id` column appearing while the record says single is the drift signal.

## Composition (§5)

Tenancy × principal = **tenant membership**; tenancy × temporality = **per-tenant
retention**. Rules authored in 5A.3.
