0009. Where multitenancy lives
0009. Where multitenancy lives
Status
Proposed
Date
2026-10-04
Deciders
open. The operator must rule, when multitenancy is scheduled.
Context
Multitenancy means one deployment serves several customers (tenants) whose data must not mix. In Ash, the Elixir framework Mesh is modelled on, it is part of the core resource language: a multitenancy section with two strategies, :context (defers to data-layer features) and :attribute (filters on an attribute such as org_id), plus per-action overrides (:enforce, :allow_global, :bypass, :bypass_all) and an all_tenants? option on identities (Ash features, section 7). Ash’s expression language also has a ^tenant template (research synthesis, section 1, “Expressions” row). The tenant travels with the actor in the scope (Ash features, section 6.9).
Mesh has not decided. The original plan (2026-10-01, not published) listed it as an open question: whether multi-tenancy is in the first version or deferred to an extension. The synthesis put multitenancy in the extension column of its ring table (research synthesis, section 15), a proposal that was never ruled on. The rulings file’s “Consequences for the proposal”, second bullet, says “tenant belongs to the multitenancy extension”, and plan revision 2 copied it; the Review note calls it the lead’s invention, not an operator ruling (rulings of 2026-10-04, Review note). The scope in ADR-0007 is { actor, context } with no tenant field. Multitenancy is not scheduled in the roadmap (roadmap, section 6).
Decision
Not decided.
Recommendation: decide when multitenancy is scheduled, not before. Until then Mesh behaves as Option C: nothing is built, and an application that needs a tenant carries it in context. Consequence for v1: M6 builds no way for an extension to add fields to the scope, the “scope contribution point” of plan revision 2 (plan revision 2 (superseded), M6; roadmap, M6, out of scope). This blocks nothing in v1.
Options considered
Option A: Tenant is a core scope field and a core data-layer concern
| Dimension | Assessment |
|---|---|
| Complexity | Highest in core: scope type, data-layer contract and expressions all change |
| Cost | Every data adapter must implement tenant isolation |
| Reversibility | Low: core gains a concept it cannot easily drop |
| Fit with ADR-0001 | Weak: many applications never use a tenant |
Pros: It is Ash’s shape. One typed scope.tenant; isolation enforced in one place.
Cons: Grows the data-layer contract, whose Ash version the research counts at 46 callbacks and 47 capability names applied inconsistently (research synthesis, section 2.2). Under ADR-0001 anything optional is an extension.
Option B: A multitenancy extension that contributes to the scope and filters
| Dimension | Assessment |
|---|---|
| Complexity | Medium: needs a scope contribution point and a way to add filters |
| Cost | Defers until the extension host (M6) is mature |
| Reversibility | Medium |
| Fit with ADR-0001 and ADR-0020 | Good |
Pros: Core stays small. Contributions go through declared points (ADR-0020).
Cons: The scope type would depend on which extensions are enabled. The extension must reach into every read and write, which is the kind of cross-extension write that Ruling 5 allows only through published points.
Option C: Not supported until needed; tenant carried in context
| Dimension | Assessment |
|---|---|
| Complexity | None |
| Cost | None now |
| Reversibility | High |
| Risk | Isolation is each application’s job |
Pros: Nothing to build or get wrong. Matches the roadmap.
Cons: Nothing enforces isolation: a forgotten filter is a data leak (this follows from the design; not in the sources). Adding tenancy later may change generated signatures and queries.
Option D: Tenancy as a data-adapter capability
Ash’s :context strategy defers isolation to the data layer (a schema or database per tenant, or Postgres row-level security).
| Dimension | Assessment |
|---|---|
| Complexity | Medium: a declared capability (ADR-0013) plus a tenant in the scope |
| Cost | Only adapters that support it implement it |
| Reversibility | Medium |
| Fit with ADR-0013 | Good: a missing capability is a build error |
Pros: Isolation enforced by the database, not by every query.
Cons: Still needs a tenant in the scope, so it does not settle Option A against B; it covers only Ash’s :context strategy, not the :attribute filter.
Trade-off analysis
A and B differ on where the concept lives, C on whether it exists. Choosing early buys nothing, since no milestone uses a tenant, and risks building a contribution point nobody exercises. Choosing late costs a possible rework of generated code, which regeneration softens.
Consequences
- No scope contribution point in M6; the extension host is smaller.
- Applications needing tenants pass them in
contextand filter themselves until this is decided. - Revisit when multitenancy is scheduled; also check how policies (M8) would express a tenant condition.
Action items
- after v1: schedule multitenancy and have the operator rule on this ADR.
- M6: do not add a way for extensions to add to the scope (roadmap, M6).
- M8: write policy examples that do not assume a tenant.