# `AshReplicant.Resource.Verifiers.ValidateMultitenancy`
[🔗](https://github.com/baselabs/ash_replicant/blob/v0.3.0/lib/ash_replicant/resource/verifiers/validate_multitenancy.ex#L1)

Compile-verifier for the `replicant` tenant sources (surfaced as a Spark
diagnostic; build-blocking under `--warnings-as-errors`). Two arms.

## `tenant_attribute`

When a `tenant_attribute` is set it is the per-row tenant discriminator Ash
injects as a plaintext filter and force-set. It fails closed unless it is:

- **not** classified `sensitive` — an encrypted discriminator would match
  nothing (fail-open isolation), and `ash_replicant` holds no key material;
- **not** listed in `skip` — a skipped discriminator is never written, so the
  tenant filter matches nothing (fail-open isolation);
- a **declared** attribute — an undeclared discriminator cannot be resolved or
  force-set;
- **not** binary-storage-typed — the discriminator is a plaintext comparator;
  a tag/base64 discriminator would scope inconsistently; and
- backed by an Ash **`multitenancy`** block — the sink passes the resolved
  per-row tenant to Ash as the `tenant:` option, which Ash HONORS only under
  declared multitenancy. With no `multitenancy` block, `tenant:` is silently
  ignored and every tenant's rows are mirrored UNSCOPED (fail-open isolation).

## `tenant_mfa`

An mfa-resolved tenant is passed to Ash as the same `tenant:` option, so it has
the **identical** "no `multitenancy` block ⇒ `tenant:` ignored ⇒ every tenant
mirrored unscoped" fail-open. This arm requires an Ash `multitenancy` block when
`tenant_mfa` is declared — any strategy satisfies (both `:attribute` and
`:context` honor `tenant:`; `:context` is the typical mfa pairing, since the mfa
value is a computed function result, not a stored attribute). The shape checks
above do not apply to the mfa arm — the tenant is a function result, not a
declared column.

## Multitenancy block `:attribute` shape

Independent of the tenant source, when the resource declares `strategy :attribute`
the multitenancy block's OWN `attribute` is force-set to the plaintext tenant on
write and filtered on read. It must be a plaintext, non-sensitive comparator — a
`sensitive`-classified or binary-storage-typed discriminator would store/compare a
mismatched value and **silently mis-scope** (reads return empty). This runs for both
tenant sources (and a global `:attribute` resource). An AshCloak-encrypted attribute
is already rejected by Ash's own multitenancy verifier (the cloak transform removes
the plain attribute), so it is not re-checked here.

Messages are value-free: they name schema structure (the attribute name or the
`tenant_mfa` key), never a row value or the mfa target's arguments.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
