Repository navigation
v1.2.0 — multi-tenancy (attribute-aligned, fail-closed)
Packaging follow-up in 1.2.1: the documentation tree (including this release's tenancy adoption checklist) now ships in the hex tarball.
Tenancy release (ADR-0011, additive and opt-in): ash_hooks aligns with
Ash attribute multitenancy — an explicit tenant threads every data call,
and the package fails closed with named errors before any data access
when a multitenant resource is touched without one. Single-tenant
adopters change nothing: the entire pre-1.2 suite passes unchanged over
tenant-less fixtures inside the now-610-test gate, because threading a
tenant through resources with no multitenancy declaration is
mechanically inert.
Added
- Tenant threading on every path —
AshHooks.dispatch/4(:tenant
opt),AshHooks.Ingress.ingest/4(ctx[:tenant]), the claim/mark/
renew/redact/prune/reap heads (:tenantopt, new optional arity),
AshHooks.Delivery.run/2(tenant recovered from job args), and both
prunes. The tenancy contract: consumers declare
multitenancy :attribute, attribute: <same attr>on the resources a
given operation touches; the package verifies that set on every entry
point and returns{:error, :tenant_required}/
{:error, :tenancy_mismatch}before any data access (Ash's native
fail-closed stays the backstop;global?: trueis rejected — it
silently disables fail-closed reads). A compile-time verifier
additionally rejectsmultitenancy :bypass,:bypass_all, and
:allow_globalactions on multitenant package resources. - Per-tenant operations —
AshHooks.reap_all/2and
AshHooks.prune_all/2sweep a tenant enumerable sequentially with
per-tenant error isolation and totals
({:ok, %{results: %{tenant => {:ok, n} | {:error, reason}}, total: n}}). AshHooks.reconcile_pending/3— the supported reconciliation for
delivery rows stranded at:pendingby a crash between row write and
enqueue: a WHERE-gated CAS flip to:enqueue_failed(real state
change — matched-records is the win signal; exactly one winner per
row among concurrent reconcilers, proven under a custom non-Oban
seam), then enqueue through the same seam dispatch takes. Exactly-once
ACROSS claim mechanisms (reconciliation vs re-dispatch repair) is the
enqueue seam's contract — the canonical Oban seam's job uniqueness
provides it; a custom seam must be idempotent itself.- Tenant-aware secret resolution (all optional) — the worker macro's
:tenant_aware_secrets(2-arityf(ref, tenant)resolver contract);
the inboundsecret fn tenant -> ... endsource; the provider
behaviour's optionalwebhook_signing_secret/2
(Organization × connection custody;use AshHooks.Providerships an
overridable default delegating to/1); and:tenantin the
verify_signature/3context map. - Oban args carry the tenant — the generated
enqueue/2serializes
the row's tenant (string tenants are the supported shape; non-string
tenants must round-tripparse_attributeover JSON); uniqueness keys
stay[:endpoint_id, :event_uuid], and a pre-tenancy tenant-less job
still conflicts with the tenant-bearing job on the same pair (proven
on the real Oban engine). - The dispatch result contract is now a public typespec
(AshHooks.Dispatcher.dispatch_result/0) including the new
:reconciledstatus. - The multi-tenant adoption checklist
(documentation/tutorials/tenancy-adoption-checklist.md) and ADR-0011.
Migration notes
Two adopter classes see action; single-tenant staying single-tenant
sees none. See UPGRADING.md and the adoption checklist: (1) cutover
with in-flight 1.1.x Oban jobs — drain before serving multitenant
dispatch (pre-tenancy args fail closed, correct but noisy); (2)
single→multi on populated tables — backfill the tenant attribute first
(NULL-tenant rows form a shadow partition that strands effect-once),
then regenerate identity indexes, then enable.