We do exactly this - our member (in person not-for-profit group) portal manages the accounts of forum members on sign-up. There are a bunch of annoying edge cases for duplicates as you mention.
So as to not be confusing: joining not-for-profit as ‘membership’ where forum is ‘user’.
Sign up for membership >> (if not exist forum user by membership.email) create forum user, set username to membership.discourseUsername, add to groups for permissions, and approve/activate account immediately.
Sometimes people join the forum to see what our community is about before becoming a member, those are manually approved.
On membership join >> (if exist forum user by membership.email), trample the forum user with the membership.email as above. It’s a strange case but there are no non-member users whose accounts would be compromised by a paying member joining the organization
Apologies that our external app is gigantic so you may need to dig to see how we pull the trigger on all those steps/api requests.