#25932·twenty

Bug: a half-removed workspace member 500s GetCurrentUser for every user in the workspace

Author: HectorBernstorffCreated Sep 15, 2026Updated Sep 15, 2026
Labelsprio: high

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:

typescript
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:

typescript
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.