ADR-0108: Mandator Membership — Invite-Accept, with Pre-Assigned Responsibilities¶
- Status: accepted
- Date: 2026-08-07
- Deciders: Nicolas Burri, through the 2026-08-07 morning discussion of the mandator dossier's section C ("I would probably go for an invite to mandator in a similar way as Microsoft has solved the tenant question"; the MDR-cadence argument overruling the supplier-bypass proposal; "include the pre-assignment in ADR-0108"; the entry-consent-not-a-lock clarification).
- Relates to: ADR-0106/0107 (assignment as the disclosure and auditability anchor — the
foundations this builds on), ADR-0098 (authority communication — the consent record's other
half), ADR-0063 (email-match binding), ADR-0012 (admin ≠ content), ADR-0004 (the global
catalog, deliberately untouched), ADR-0097 (provisioning — its delegation is NOT decided here),
docs/planning/mandator-system-options.md§B/§C, register 2026-08-06 #7 (subsumed).
Context¶
The target operating model (Nicolas, from the operating reality of a software supplier): supplier employees work for several mandators in parallel and must see only the mandators they are assigned to; customer employees see only their own mandator; within a mandator, scope-level reach keeps following the VIEW model as today. Staffing cadence is MDR-shaped — mandators are few, teams stable, and joining one is a controlled event (competence evidence, org-chart placement), not logistics.
What exists: staffing is already scope-delegated (assignRole gates and audits at the target
scope); listUsers carries a restricted branch; ADR-0107 made audit anchoring follow staffing
reach. What is missing: "belongs to mandator X" is only derived from role assignments — there is
no membership state, no way to be staffable-but-unstaffed, no entry record — and the user
lifecycle acts (invite/deactivate) are GLOBAL-anchored, so no tenant can onboard its own people
(register #7: the invite also creates no login account).
Decision¶
- Membership is first-class. A person's relation to a mandator is an entity with a lifecycle
—
INVITED → ACCEPTED → REMOVED, withDECLINEDas a visible terminal state — audited at the mandator. Membership means: staffable there, and listed in that tenant's user administration. Roles keep meaning reach (the VIEW model is untouched): a member with no assignments sees nothing. Content disclosure does not move one millimeter in this ADR. - One path for everyone: invite → accept. Adding a person to a mandator they do not belong to is an invitation the person accepts — including supplier staff. The rejected alternative (a routine operator bypass for the supplier lane) would have exempted exactly the highest-cross-tenant-reach population from the consent record. At MDR cadence the ceremony is affordable and the event significant enough to deserve one. No skip-acceptance configuration (opinionated-system principle; a proven high-churn tenant is the documented trigger to revisit). The operator's impose path remains for migration and break-glass only — rare, loud, audited.
- Persons are global; email is the match key, never the identity. One person per email at invite time (ADR-0063 binding unchanged); the person's other memberships are never disclosed across tenants — an inviter cannot learn whether an email already exists in the system (anti-enumeration; the inviter sees "invited, pending" either way).
- Invites evaluate at REDEMPTION, not at send. The invite stores email + target mandator + pre-assignments + token; everything else resolves when it is redeemed: a logged-in redeemer accepts as their session identity (email drift cannot mint duplicates); an unknown email takes the new-person branch (Keycloak account creation with email verification — this is where register #7 dissolves); an operator-imposed membership auto-resolves a pending invite as fulfilled.
- Pre-assigned responsibilities are part of the invite. An invite may carry (scope, role) pairs; acceptance applies them in the same recorded act — membership + staffing + acknowledgment of named responsibilities (the ADR-0098 communication story, embedded in the entry act). Pre-assignments validate at acceptance; an unfillable pair (role no longer activated) degrades to a flagged gap on the admin's board — the membership itself always completes.
- Acceptance is the ENTRY consent, not a lock. Within a standing membership, staffing changes — same or different roles, other scopes of that mandator, removals — are direct admin acts with no re-ceremony. The person's ongoing protection is visibility: the trail, their own reflection ("My access"), the authority view, and a notification to the assignee on changes.
- Lifecycle asymmetry. Tenant administrators end membership (which unassigns everything in their subtree); only the operator ends existence (deactivation) — a member of several mandators is never killed by one of them.
- Hygiene, branch-asymmetric. Account-minting links (new-person branch) expire short (~14–30 days — a stale minting link is the model's one real risk); in-app membership offers may sit long (they grant nothing until an authenticated session accepts; 90 days for tidiness). Re-send is idempotent (new token, old token dies); revoke exists; declined is visible to the inviter, without a reason field.
Consequences¶
- Schema: a membership table (person, mandator, state, timestamps, inviter) + the invite
(token, email, pre-assignment pairs); events
MEMBER_INVITED / ACCEPTED / DECLINED / REMOVEDanchored at the mandator (ADR-0107's reach gate already admits every writer involved). Migration derives memberships ONCE from existing assignments, then the derived shortcut is retired. - Re-anchoring: the invite act moves from MANAGE_USERS@GLOBAL to MANAGE_USERS at the target
mandator;
listUsers' restricted branch becomes the tenant-admin listing over memberships. Provisioning delegation (scope birth by tenant admins) is deliberately NOT decided here — section C2 of the dossier, its own ADR when wanted. - Onboarding seams, declared not built: membership events are first-class trail citizens so a
mandator's onboarding process (training obligations, org-chart placement prompts, ack policies)
can attach to
MEMBER_ACCEPTEDlater without remodeling. - Realization in two waves: wave 1 = the in-app branch (existing users, inbox invitation, accept/decline — no SMTP; serves the supplier-internal case immediately); wave 2 = the email branch (SMTP + KC action tokens — serves real customer onboarding). Bulk invite (multi-email input, acceptance strictly per person) is a wave-1 convenience, one dialog, no model change.
- The fresh-install journey grows a station when wave 1 lands: bootstrap → first mandator → invite → accept → staffed — the full person-birth path as PQ evidence.
- Implementation queued — does NOT start with this ADR (spec-first; the dossier's M1 journey evidence and this ADR's realization order remain Nicolas's scheduling call).
Amendment 1 (2026-08-22) — §2's "no impose path" was a promise, not a property; now it is enforced¶
The 2026-08-22 smoke round found assignRole checked the caller's per-scope MANAGE_USERS, the
target's existence, non-EXTERNAL status and role activation — and nothing about the target's
relation to the organization. Since a bare role assignment IS belonging in this system
(staffedInMandator, the pre-ADR-0108 meaning V081 derived memberships from), the endpoint was an
executable impose path: a tenant administrator with any user id could bind a person from another
customer's organization into their own scope — content reach granted, roster appearance (an
existence oracle against §3), derived membership minted, no consent, no invitation trail.
Now gated in the shared staffing body: staffing requires the target to have STANDING in the
scope's organization — an open membership (INVITED/ACCEPTED) or an existing role there —
refused otherwise with a named 409. Two exemptions, argued in code: GLOBAL (no organization to
cross) and self-staffing (actor == target; consent is not at issue with yourself, and the
setup wizard's staffing bridge depends on it). Deliberately unchanged and open for a separate
ruling: staffing an INVITED-but-not-yet-ACCEPTED member is still permitted, because the assignment
surface offers that case — offer and enforcement resolve one rule either way.
Amendment 2 (2026-08-23) — staffing a pending member lands as a pre-assignment, never a live row¶
The 2026-08-22 cross-organization gate deliberately mirrored the assignment picker: INVITED
members could be staffed (open decision 21). Writing the decision up surfaced the fact that
settles it: assignment rows are live immediately — the authorization resolution (V005) reads
role_assignment with no membership-state join — so staffing a pending member granted real,
effective reach into an organization the person had not yet consented to joining. That is not an
administrative convenience; it is §2's consent model with a hole in it.
Ruled (Nicolas, option a): staffing a person whose membership is INVITED lands as a pre-assignment on the open invitation — same dialog, same administrative act, but it joins the offer and takes effect AT ACCEPTANCE in the same recorded act, exactly like §5's invite-time pairs. One semantics for "responsibilities offered before consent"; zero live reach without a yes. The assignment surface says which of the two it just did. Realization must decide what happens to any live rows already held by pending members at migration time (convert to pre-assignments is the consistent answer) and say so loudly.