The tenancy contract resolver: every public entry family resolves the resources it touches BEFORE any data access.
- a single-tenant set (no resource declares multitenancy) passes
the tenant through unchanged — inert by construction: Ash applies a
tenant only when a strategy is declared (reads no-op without one,
and the data layer ignores tenants except under
:context), so threadingtenant:unconditionally cannot change single-tenant behavior; - a consistent attribute-multitenant set — every touched resource
declares
multitenancy :attributeover the SAME attribute withglobal?: false— requires a tenant: none given is the package's named{:error, :tenant_required}(Ash's own TenantRequired stays the backstop on paths this pre-flight cannot see — the bang-path heads this guard converts to tuples would otherwise crash); - anything else is
{:error, :tenancy_mismatch}before any data access: some-but-not-all declared (an undeclared resource reads globally next to tenant-scoped ones), differing attributes, a non-:attributestrategy (not portable across the package's supported data layers), orglobal?: true(which silently disables Ash's fail-closed reads — it passes a same-attribute consistency check while permitting tenant-less access).
The cross-tenant hazard the mismatch guard closes is concrete: an inconsistent set reached through the delivery runtime would resolve an endpoint GLOBALLY under a tenant-scoped row and deliver one tenant's payload bytes to another tenant's URL.
Three Ash.Resource.Info introspections per resource per call,
deliberately uncached: a compile-time cache would race Spark's
cross-resource compilation.
Summary
Functions
Resolves the entry family's declaration set against the caller's tenant.
Types
Functions
Resolves the entry family's declaration set against the caller's tenant.
modules are the resources the entry family is about to touch (data
access through each follows immediately after); tenant is the
caller-supplied tenant, nil when none was given.