
Keep a usable owner in the workspace
An organisation needs an owner who can actually act. Wildo checks the remaining usable owners before allowing membership changes that would remove the last one.
The normal remedy is to appoint another owner first. This protects everyday administration while keeping account suspension and explicit data-erasure decisions distinct.
Example — Transfer ownership before leaving
The only owner tries to remove their membership. The change is refused until another active member with an active account has a role that confers ownership.
For engineers
Count authority, not the text of one role
A usable owner combines a usable membership, a usable linked user and a role that confers ORG_OWNER through the current role hierarchy. A role label on an invited or inactive membership does not satisfy the floor.
| Candidate | Counts as a usable owner? |
|---|---|
| Active membership, active user, owner-conferring role | Yes |
| Invitation carrying an owner role | No |
| Active membership whose user is inactive | No |
| Custom role that inherits ownership, with usable membership and user | Yes |
Let the existing membership operation enforce it
The create/update/removal implementations call the owner-floor authority as appropriate. Application code should use the normal membership service path rather than editing the repository to bypass it.
The guard measures the owners the proposed change would remove. This selected excerpt from organization-owner-floor.backend.utils.ts runs after the transaction has established the serialization boundary; intervening comments are omitted:
const usableOwnerMemberIds = await measureUsableOrganizationOwners(params);
const reducedMemberIdSet = new Set(reducedMemberIds.map((memberId) => String(memberId)));
const reducedUsableMemberIds = usableOwnerMemberIds.filter((memberId) => reducedMemberIdSet.has(memberId));
if (reducedUsableMemberIds.length === 0) return undefined;
const remainingUsableOwners = usableOwnerMemberIds.length - reducedUsableMemberIds.length;
if (remainingUsableOwners >= ORGANIZATION_OWNER_FLOOR_MINIMUM) return undefined;
return { organizationId, reducedMemberIds: reducedUsableMemberIds, remainingUsableOwners };
An empty result means this change does not breach the floor. A returned breach identifies the affected organisation and reductions for the caller’s refusal; it is not permission to proceed.
The guard serialises competing reductions on the organisation record inside the transaction. Two removals that each observe a second owner must not both commit and leave zero. This is why a separate preflight count in a custom UI is only guidance, not enforcement.
Give the refusal an actionable recovery path
A refused reduction returns HTTP 409, with error.code set to LAST_ORGANIZATION_OWNER in the HTTP response. Use the public enum to distinguish it from an unrelated conflict. For example, this application-owned predicate can select the ownership-transfer message in a custom administration screen:
import { AdministrativeContinuityErrorCode } from '@wildo-ai/saas-models';
export function needsAnotherOrganizationOwner(
status: number,
errorCode: unknown,
): boolean {
return (
status === 409 &&
errorCode === AdministrativeContinuityErrorCode.LAST_ORGANIZATION_OWNER
);
}
Pass the HTTP status and the parsed response’s error.code; keep the normal handling for other failures. Do not match the human-readable message or the internal audit reason. This conflict needs a change in ownership, so automatically retrying the same request is not a remedy.
Guide the administrator to give an active organization-wide member an owner-conferring role, confirm that member’s linked account is active, and then retry the original operation. Preserve the target member’s other required roles when updating their role set. A pending owner invitation is insufficient until accepted. An owner grant on an organization unit is also insufficient: the floor measures organization-wide memberships, not unit assignments.
The server still evaluates the final write in its transaction. A UI that shows a second owner is helpful guidance, not proof that concurrent changes cannot alter the result.
Keep distinct decisions distinct
Platform user deactivation can stop a compromised account even when that strands an organisation; the affected organisation is reported for repair. An explicit subject-erasure decision has its own acknowledged-breach path. Neither should be described as an ordinary membership administrator’s permission to remove the final owner.
If the organisation is already stranded, use the ownership recovery operation. It promotes a usable existing member; it does not weaken the regular floor.