the-custodian/research/2026-08-17-adr008-plane-p-placement.md
codex 04f888399f research: external survey of multi-tenancy practice, one digest per ADR-008 plane
Four search-and-read passes against AWS Well-Architected SaaS Lens, Azure
Architecture Center, OWASP, the OpenID Foundation, and PostgreSQL practitioner
literature. Each digest holds one of our ladders against outside practice and
lists its sources.

Headline: the draft-1 -> draft-2 reframe is corroborated. Microsoft states
"isolation is a spectrum, not a discrete property" and derives our E/P coupling
independently. The shape is sound; the details are not.

Five corrections draft-2 needs:

- E3 overclaims. Any session can re-SET the GUC, so RLS is strong against a
  forgotten predicate and useless against SQL injection or a compromised
  process. The ADR says "the application cannot trivially route around" - the
  exact overclaim it prohibits elsewhere.
- The pooling claim in section 16 is backwards. Transaction-scoped SET LOCAL is
  what makes E3 safe under pooling; statement-level pooling is what serves
  other tenants' rows, only under production concurrency.
- E3 layers on E2 rather than superseding it.
- Placement cannot be expressed per tenant, but the commercial section requires
  exactly that - a tier bought by some tenants puts a service at two P levels
  at once, which the posture vector cannot represent.
- FORCE ROW LEVEL SECURITY is mandatory: ADR-0001 already established that our
  migration role owns the tables it creates, so it would silently bypass every
  policy.

Also: pgTAP and pgrls make the E3 evidence artifact far cheaper than assumed;
OWASP publishes a Multi-Tenant Security Cheat Sheet we reference nowhere; our
tenant-boundary gap is BOLA, OWASP API1, top of that list since it launched;
AuthZEN 1.0 went Final in January and flex-auth is pre-standard; and no plane
covers tenant offboarding, which OWASP makes a MUST.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 13:00:02 +02:00

14 KiB
Raw Permalink Blame History

Plane P — Placement: prior knowledge and best practices

Research artefact — 2026-08-17. Supports canon/architecture/adr-008-multi-tenancy-model.md (draft-2), plane P of four. Surveys external practice for which substrate holds a tenant's data, and holds our P0P4 ladder against it. Contains strong external validation of draft-2's reframe, and one expressiveness failure in the P ladder itself. Companions: plane I (identity), plane A (authorization), plane E (enforcement).

1. What the plane covers

Which physical substrate holds a tenant's data, who decides, and what moving costs. Our ladder:

Level Shape
P0 Shares a database with another consumer
P1 Database per consumer, shared cluster
P2 Dedicated cluster per consumer
P3 Dedicated cluster per tenant
P4 P3 plus separate region or jurisdiction

2. Validation: "isolation is a spectrum, not a discrete property"

The operator's instruction to reframe draft-1 from a fixed model into a graduated framework is not merely defensible — it is Microsoft's stated position, in almost the same words:

"Instead of viewing isolation as a discrete property, consider it a spectrum. You can deploy components of your architecture that are more isolated or less isolated than other components in the same architecture, depending on your requirements." — Azure Architecture Center — Tenancy models for a multitenant solution

The same page decomposes isolation across tiers exactly as our planes do — "you can use different levels of isolation for each tier", with a shared UI tier, a shared middle tier, and isolated databases given as the worked example.

Against our model: draft-2's core move is externally corroborated by the most rigorous public taxonomy available. Worth citing in ADR §15, because it converts "we decided to do it this way" into "this matches the reference guidance", which matters for reviewers who will ask whether we invented a private vocabulary.

3. Validation: our E↔P coupling claim is independently confirmed

ADR §3.2 asserts that the planes couple at the top — E4 is only reachable at P3 or above — and §4.4 asserts that P0 → P1 → P2 does not raise the E level. Microsoft states the mechanism plainly:

"A key difference between tenants and deployments is how isolation is enforced. When multiple tenants share a single deployment (a set of infrastructure), you typically rely on your application code and a tenant identifier that's in a database to keep each tenant's data separate. When tenants have their own dedicated deployments, they have their own infrastructure, so it might be less important for your code to account for a multitenant environment." (ibid.)

That is our E/P relationship derived independently: shared deployment ⇒ enforcement is application code (our E1E3); dedicated deployment ⇒ enforcement is structural (our E4).

4. Finding: our P ladder cannot express per-tenant placement, and our own commercial section requires it

This is the significant gap.

Azure names two partitioning axes that our single ladder collapses:

  • Vertically partitioned deployments — "a combination of single-tenant and multitenant deployments. For example, you might have most of your customers' data and application tiers on multitenant infrastructures, but you deploy single-tenant infrastructures for customers who require higher performance or data isolation." Explicitly monetisable: "You can even charge customers a higher rate to use a single-tenant deployment." (ibid.)
  • Horizontally partitioned deployments — shared application tier, dedicated databases per tenant, targeted at the component that carries the load: "if you identify that specific components cause most of the load on your system, then you can deploy separate components for each tenant." (ibid.)

Our P ladder assigns one level per service. tenant-engine is P1. audit-core is P1. There is no way to say "P1 for most tenants, P3 for the three enterprise tenants who bought isolation" — which is precisely the vertically partitioned model, and precisely what ADR §11's minimum-level plan tiers imply.

The contradiction is internal to draft-2: §11.1 says "a tier may require E3 P2", but if only some tenants hold that tier, the delivering service occupies two P levels at once. The posture vector as specified cannot represent that state, so a service in it would have to either overclaim (declare P2 for everyone) or underclaim (declare P1 and misrepresent what the tier delivers). Both are §6 violations.

Proposed amendment. Either:

  1. Make P a per-tenant-class level rather than a per-service one — the vector carries a default plus exceptions:
    tenancy:
      current: { I: 2, A: 3, E: 2, P: 1 }
      placement_exceptions:
        - tenants: ["tenant:enterprise:*"]
          P: 3
          reason: "isolation tier"
    
  2. Or declare per-tenant placement out of scope for the framework and say so, accepting that plan tiers selling isolation are then unmodellable.

Option 1 is more work and is the honest one, because the estate has already anticipated tiered isolation. Azure also flags the cost: "your codebase needs to be designed to support both multitenant and single-tenant deployments", and migration between them must be planned for (ibid.) — which is exactly what our §7 portability rules already provide, so we are better positioned for this than most.

5. Cell-based architecture: the pattern our ladder skips

ADR §18.8 asks whether to evaluate cells. The literature gives enough to sharpen the question.

  • A cell is "a complete, independent instance of the service" with a fixed maximum size — compute, storage, supporting services and configuration for a defined blast radius (Rackspace — cell-based architecture on AWS, AWS guidance for cell-based architecture).
  • Cells are logical partitions on a partition key — customer ID ranges or tenant ID — and "the data required for or generated by each cell is also partitioned, so that there is no replication of data between cells" (ibid.).
  • The sizing argument is the point: "by capping the size of each cell… you can potentially predict and address failures more easily. This leads to a higher MTBF" (ibid.). And "it is important to identify the proper partition key to minimize cross-cell interactions" (ibid.).
  • Cells are the bulkhead pattern applied to tenancy — AWS files it under fault isolation, REL10-BP04 (AWS Well-Architected — use bulkhead architectures to limit scope of impact).
  • Azure's equivalent is the Deployment Stamps pattern: "each stamp serves a predefined number of tenants, allowing you to scale almost linearly and serve increasing numbers of tenants" (Azure — Deployment Stamps pattern). Azure also calls deployments "supertenants or stamps" and describes the tenant→deployment mapping table as a first-class component.

Against our model: a cell is not a rung on our P ladder — it is a different axis. P measures how much is shared; a cell measures how large a shared unit is allowed to get before you make another one. Our platform-pg is, in cell terms, an uncapped cell: §16 of the ADR computes a ceiling of roughly six consumers but nothing enforces it, and there is no second cell to overflow into.

That reframes §18.8 usefully. The question is not "should we adopt cells" but "what is platform-pg's declared maximum size, and what happens at the boundary?" — a question we can answer now, cheaply, and which the connection ceiling already forces.

Azure also names bin packing as the assignment strategy and stresses maintaining "a record of your customers and the infrastructure where their data and applications reside so that you can route their traffic to the correct location" — a tenant→deployment mapping table. We have no such record; the placement facts in ADR §1 were assembled by inspection.

6. Placement is a commercial decision, and the guidance says so first

Our §11 treats commercial expression as a constraint to be managed. Azure puts it the other way round:

"Selecting a tenancy model isn't only a technical decision. It's also a commercial decision." — considering business objectives, compliance, scale, automation capacity, and SLAs (ibid.).

And on the economics, matching our §14 rejection of R3/P3-everywhere: "if a single tenant requires a specific infrastructure cost, 100 tenants probably require 100 times that cost", with ongoing maintenance time-consuming at scale (ibid.). Conversely single-tenant is appropriate "if you expect that your business will have only a few customers… even though it's more costly".

Against our model: ADR §8.2 proposes placement ownership to railiance-platform, an operations-shaped repo. This guidance suggests placement policy is jointly commercial and technical — which strengthens the case that adaptive-pricing must be a co-signer on §18.2, not only on §18.5.

7. Our tenant taxonomy is a recognised hard case

Azure's B2C guidance describes our exact situation:

"If your customers are consumers, it's often more complicated to relate customers, tenants, and users… consider whether your solution might be used by families, groups of friends, clubs, associations, or other groups that might need to access and manage their data together. For example, a music streaming service might support both individual users and families, and it might treat each of these account types differently when it separates them into tenants." (ibid.)

Our ADR-0013 taxonomy contains consumer, family, community, and association verbatim. It also notes that if tenants are individuals, "you might need to consider how you handle personal data and about the data sovereignty laws in each jurisdiction" — which is our P4 and, combined with plane A §6, the tenant-deletion gap.

8. Testing placement is a named obligation

"Whichever isolation model you choose, be sure to test your solution to verify that one tenant's data isn't accidentally leaked to another and that any noisy neighbor outcomes are acceptable." (ibid.)

Two evidence artifacts, not one. ADR §13 covers the leak half (P1P4: "provisioning declaration plus the platform's own isolation probes") and says nothing about noisy-neighbour behaviour. Azure suggests deliberate fault injection (Chaos Studio) to verify resilience.

Proposed amendment to §13: add a noisy-neighbour evidence artifact for P1 and P2 — a demonstration that one consumer saturating its connection or CPU allowance does not breach another's. rapp-postgres already sets database-scoped connection_limit, statement_timeout and idle_in_transaction_session_timeout precisely for this, and ADR-0001 §3 argues resource exhaustion is an isolation failure — but nothing proves the limits hold under load.

9. Summary against our ladder

Finding Verdict
"Isolation is a spectrum, not a discrete property"; different tiers at different levels Validates draft-2. Cite in §15.
Shared deployment ⇒ app-code enforcement; dedicated ⇒ structural Confirms E↔P coupling independently.
Vertical partitioning: some tenants pooled, some dedicated, explicitly monetised Gap. P is per-service; cannot express per-tenant placement that §11 requires.
Horizontal partitioning: shared app tier, per-tenant DB for the hot component Unmodelled. Our P assumes whole-service granularity.
A cell has a fixed maximum size; stamps scale near-linearly Reframes §18.8. Ask "what is platform-pg's declared max size?", not "should we adopt cells".
Tenant→deployment mapping table is a first-class component Missing. Our placement facts were assembled by inspection.
Tenancy model is a commercial decision as much as technical Amend §8.2. adaptive-pricing should co-sign placement ownership.
B2C/family/association tenancy is a recognised hard case Our taxonomy hits it verbatim; jurisdiction concerns feed P4.
Test for leakage and for acceptable noisy-neighbour outcomes Amend §13. No noisy-neighbour evidence artifact exists.

Sources