EB
You're right on both, thanks Mike. Wrapping the column in a function means the email index can't help, and two concurrent exists checks can both pass before either user row commits. I'm changing step 6 to the table you describe: the normalised email as the primary key, insert on conflict do nothing in the hook, and refuse when no row went in. I'll add a backfill from existing users and a note on clearing a key when account creation fails, or when someone deletes their account and should be allowed back. The Lovable prompt gets the same change.
