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>
244 lines
14 KiB
Markdown
244 lines
14 KiB
Markdown
# 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 `P0–P4` 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 E1–E3); 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 (P1–P4:
|
||
"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)
|