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

244 lines
14 KiB
Markdown
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.

# 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](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/considerations/tenancy-models)
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:
```yaml
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](https://www.rackspace.com/blog/cell-based-architecture-aws),
[AWS guidance for cell-based architecture](https://github.com/aws-solutions-library-samples/guidance-for-cell-based-architecture-on-aws)).
- 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](https://docs.aws.amazon.com/en_us/wellarchitected/2022-03-31/framework/rel_fault_isolation_use_bulkhead.html)).
- 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](https://learn.microsoft.com/en-us/azure/architecture/patterns/deployment-stamp)).
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
- [Azure Architecture Center — Tenancy models for a multitenant solution](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/considerations/tenancy-models)
- [Azure Architecture Center — Architectural approaches for a multitenant solution](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/approaches/overview)
- [Azure Architecture Center — Deployment Stamps pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/deployment-stamp)
- [Azure Architecture Center — Considerations for updating a multitenant solution](https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/considerations/updates)
- [AWS Well-Architected — REL10-BP04 Use bulkhead architectures to limit scope of impact](https://docs.aws.amazon.com/en_us/wellarchitected/2022-03-31/framework/rel_fault_isolation_use_bulkhead.html)
- [AWS Well-Architected — How do you use fault isolation to protect your workload?](https://wa.aws.amazon.com/wellarchitected/2020-07-02T19-33-23/wat.question.REL_10.en.html)
- [AWS Solutions Library — Guidance for cell-based architecture on AWS](https://github.com/aws-solutions-library-samples/guidance-for-cell-based-architecture-on-aws)
- [Rackspace — Cell-based architecture on AWS](https://www.rackspace.com/blog/cell-based-architecture-aws)
- [Ryan Harrison — Cell-based architecture engineering guidelines](https://guidelines.ryanharrison.co.uk/architecture/cell-architecture/)
- [Anudeep Balla — The guide to cell-based architecture](https://medium.com/@anudeepballa7/the-guide-to-cell-based-architecture-58cf607f9754)
- [AWS Well-Architected SaaS Lens (PDF)](https://docs.aws.amazon.com/pdfs/wellarchitected/latest/saas-lens/wellarchitected-saas-lens.pdf)
- [hidekazu-konishi — AWS SaaS multi-tenant architecture guide: tenant isolation, pool and silo models](https://hidekazu-konishi.com/entry/aws_saas_multi_tenant_architecture_guide.html)