{"record":{"id":"9e9e56f1acbf44c2","repo":"toeverything/AFFiNE","slug":"cannot-remove-the-last-active-workspace-owner","errorCode":null,"errorMessage":"Cannot remove the last active workspace owner.","messagePattern":"Cannot remove the last active workspace owner\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/backend/server/src/models/permission-write.ts","lineNumber":254,"sourceCode":"    const existingOwners = await this.db.workspaceMember.count({\n      where: {\n        workspaceId,\n        role: 'owner',\n        state: 'active',\n        userId: { not: userId },\n      },\n    });\n    const deletingOwner = await this.db.workspaceMember.count({\n      where: {\n        workspaceId,\n        userId,\n        role: 'owner',\n        state: 'active',\n      },\n    });\n\n    if (deletingOwner > 0 && existingOwners === 0) {\n      throw new Error('Cannot remove the last active workspace owner.');\n    }\n\n    return await this.db.workspaceMember.deleteMany({\n      where: { workspaceId, userId, state: 'active' },\n    });\n  }\n}\n\n@Injectable()\nexport class WorkspaceInvitationModel extends BaseModel {\n  @Transactional()\n  async set(\n    workspaceId: string,\n    userId: string,\n    role: WorkspaceRole,\n    status: WorkspaceMemberStatus,\n    data: {\n      source?: WorkspaceMemberSource;","sourceCodeStart":236,"sourceCodeEnd":272,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/models/permission-write.ts#L236-L272","documentation":"Thrown by WorkspaceMemberModel.delete (permission-write.ts:254) when removing a user would leave the workspace with zero active owners. The method first takes a pg_advisory_xact lock and a SELECT ... FOR UPDATE on owner rows, then counts other active owners; if the target is an owner and no other active owner exists, it refuses the delete so a workspace is never orphaned. Note this is a plain Error (not a UserFriendlyError), so unless a higher layer maps it, it surfaces as an internal_server_error.","triggerScenarios":"Calling the workspace member removal / 'leave workspace' / revoke-permission path for the only active owner of a workspace. Reproduce: create a workspace (you become the sole owner), invite no second owner, then call WorkspaceMemberModel.delete(workspaceId, ownerUserId).","commonSituations":"The sole workspace owner clicks 'Leave workspace'; an automated offboarding script removes members and accidentally targets the owner; a prior ownership transfer failed silently so the delete now strands the workspace.","solutions":["Promote another active member to owner first via WorkspaceUserModel.setOwner(workspaceId, otherMemberId) before calling delete.","If the intent is to leave, transfer ownership to a different active member, then retry the removal.","If the workspace should be destroyed, delete the workspace itself rather than removing its owner."],"exampleFix":"// before\nawait models.workspaceMember.delete(wsId, ownerUserId);\n// after\nawait models.workspaceUser.setOwner(wsId, otherMemberId);\nawait models.workspaceMember.delete(wsId, ownerUserId);","handlingStrategy":"validation","validationCode":"async function safeRemoveOwner(models, wsId: string, userId: string) {\n  const otherOwners = await models.db.workspaceMember.count({\n    where: { workspaceId: wsId, role: 'owner', state: 'active', userId: { not: userId } },\n  });\n  if (otherOwners === 0) {\n    throw new Error('Refusing to remove the last active workspace owner; transfer ownership first.');\n  }\n  return models.workspaceMember.delete(wsId, userId);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Before removing an owner, ensure at least one other active owner remains.","Route 'leave workspace' for the current owner through setOwner transfer first.","Disable the UI 'Leave workspace' control when the current user is the only active owner."],"tags":["workspace","permissions","ownership","invariant"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}