An organisation removes a member. Routine administration: the person has left, their seat is freed, their access to that organisation's training ends. Then the person cannot log in — and not to the organisation that removed them. To their own account, the one that owns their own organisation somewhere else entirely.
Nothing failed on the way there. The removal did what it was asked to do; the way back the operator reached for next — an invitation, minted and accepted — answered 201 Created.
The check could not see the row that mattered
Addresses are unique per tenant. A customer's data lives in its own tenant, and the uniqueness constraint on an email address is scoped to one. So a collision check written as “does this address already exist here?” cannot see the row that matters: the query carries the acting tenant in its own condition, and by construction the row that matters is in a different one. The check was not weak. It was asking about the wrong population, and it answered that question correctly every time.
The accept therefore created a second active account on an address that already had one. Login then refuses to proceed: when more than one row shares an address it resolves only if exactly one of them is active, and otherwise refuses rather than mint a token for an identity it picked. Two active rows means both accounts are refused, from the next attempt onward.
One door, or every door
The obvious repair is to fix the door where the problem was found. Here that would have been wrong twice over.
Nine paths into the platform's multi-tenant account store create an account. Enumerating them found the same tenant-local question behind five more doors, none of which anybody had met the bug through. The enumeration also found one the original ticket's own table did not name — the path that provisions a whole new tenant, which had no collision check of any kind. The door nobody thinks of is exactly the one a per-door fix leaves open.
The nine, in the words a reader would use for them:
public sign-up
an administrator registering a person into their organisation
an administrator creating a user directly
a bulk import, up to five hundred addresses in one request
an invitation accepted
a first sign-in through a customer's single sign-on
a hire arriving from an HR system's webhook
provisioning a new tenant
a sign-in through Telegram
Eight of them now ask one question, through one privileged read that can see across tenants: does this address already belong to somebody, anywhere? Seven ask it of active accounts; public sign-up asks the stricter form and refuses on any row at all. The ninth door is exempt, and the exemption is a claim about the address rather than about the door: that path synthesises an address that carries the tenant inside it, so no other tenant's row can hold it. The claim is pinned by a test, so if the format ever changes the exemption becomes a failing statement instead of a quiet one.
The guard is the census, not the fix
The fix closes today's instance. What keeps it closed is a list, checked from both sides on every run: a path that creates an account and is not declared fails, and a declaration whose function no longer exists fails too. A tenth door — and new ones do arrive, through ported code, SSO integrations and admin surfaces — has to be added to the list deliberately, and adding it without the shared question fails the census.
A count would not have done this job. A guard that asserts “at least N call sites ask the question” stays green while the one that matters disappears, because another one was added meanwhile. The doors are named, not counted — which is also why the number in the paragraph above is written for a reader here and relied on nowhere in the code.
What it cost, said plainly
Three limits, because a fix described without them is an advertisement:
The shared read is narrower than two of its callers. The privileged read and two of the doors do not normalise an address identically, so a small class of spellings is not yet covered by the global question, and for those the lockout is not yet closed. Fixing it means folding the normalisation inside the privileged read and backfilling the rows that already exist — a migration, which travels on its own and is filed as its own card, still open. It is named in the code beside the predicate, where the next person to read it will meet it.
Closing a trap removed a way in. A person with an account elsewhere and no history with the organisation now has no way in through an invitation, where before they had one that broke their login. That is a gap you can see instead of one you fall into, and it was closed from the correct side the same day: a second kind of accept, performed by the person while signed in, attaching their own account. The authority then comes from the account holder — the only party who can grant it — rather than from whoever is holding the invitation link.
A global answer is an existence oracle. An authenticated administrator can now learn that an address has an account somewhere. That was traded knowingly: a lockout is a certain harm to a real person, and the same answer was already obtainable from public sign-up. The unauthenticated refusal was given a budget — and the budget counts refusals only, never accepts, because keying it the usual way would have throttled a cohort of thirty students accepting one invitation from one campus network.
Why a buyer should ask about this
If you are evaluating a platform for a university, a hospital group or a chain of training centres, the useful question is not whether the vendor checks for duplicate addresses. Every vendor checks. The question is whether the check is one thing that every path shares, and whether anything fails when a tenth path appears beside them — because paths do appear, and the one that skips the check will be the newest one, added by somebody who had no reason to know the check existed.
The work above is in VARGATES Medical, where an organisation's people, their training and their records belong to that organisation and to nobody else.