Bug: a half-removed workspace member 500s GetCurrentUser for every user in the workspace
Environment
Self-hosted, Twenty v2.36.0 (built from tag twenty/v2.36.0).
Bug description
UserResolver.workspaceMembers (packages/twenty-server/src/engine/core-modules/user/user.resolver.ts) maps every active workspaceMember entity to an active userWorkspace row and throws when one is missing:
if (!isDefined(userWorkspace)) {
throw new Error('UserEntity workspace not found');
}The half-removed state is reachable in practice: userWorkspace soft-deleted while the corresponding workspaceMember row is still active (e.g. a removal that happens in two steps, or an admin deactivating a user partially). While one member is in that state, the GetCurrentUser query — which every client fires on load — fails for every user in the workspace, not only for the affected one. It looks exactly like "authentication is randomly broken": sign-in succeeds, the app shell fails to load.
We hit this in production: a member's userWorkspace was soft-deleted on one day and the workspaceMember row was only soft-deleted three days later; during the whole window every GetCurrentUser request 500ed with UserEntity workspace not found (35 logged failures, multiple users affected).
Expected behavior
The member list should tolerate the torn state: a member whose userWorkspace is already soft-deleted is mid-removal, and the list should simply skip them until both rows are gone — instead of failing the whole workspace's app shell.
Suggested fix
Skip instead of throw in the workspaceMembers resolver:
const toWorkspaceMemberDtoArgs =
workspaceMemberEntities.flatMap<ToWorkspaceMemberDtoArgs>(
(workspaceMemberEntity) => {
const userWorkspace = userWorkspacesByUserIdMap.get(
workspaceMemberEntity.userId,
);
// a member whose userWorkspace is already soft-deleted is mid-removal —
// leave them out of the list rather than failing everyone's GetCurrentUser
if (!isDefined(userWorkspace)) {
return [];
}
// ...
},
);We are running this exact change in production (with a unit spec: half-removed member is skipped; the roles not found throw for an active pair is kept). Happy to open a PR if you'll have it.
Source: twentyhq/twenty