Auth: email-domain matching for institutional auto-link¶
When a trainer signs up self-serve with an institutional email (@bit.edu.in), Kwilo checks whether that domain belongs to a verified org and offers to connect them. Org admins verify their domain once via DNS TXT, matched users get a "Join BIT?" prompt at signup, and unmatched institutional domains feed a pending-match list for sales. This closes the gap left by auth-entry-consolidation.md, where self-serve trainers at Kwilo-using institutions still landed in a personal workspace.
How it works¶
Two tables drive it. institutional_email_domains maps a verified domain to an organization_id (with verified_at, verification_token, added_by, added_via). pending_domain_matches records unverified institutional signups as a sales signal.
On signup with anita@bit.edu.in:
- If the domain is in
FREE_EMAIL_DOMAINS(gmail.com,outlook.com, etc.), skip entirely. - Else look up the domain in
institutional_email_domainswhereverified_at IS NOT NULL. - Hit: queue the post-signup prompt with the matched org.
- Miss: insert into
pending_domain_matches.
Match prompt, never silent join¶
A trainer who matches a verified domain sees a confirmation screen after signup:
Your email matches Bangalore Institute of Technology.
Would you like to join their Kwilo workspace?
[ Yes, join BIT ] [ Keep my personal account ]
Yes calls POST /organization-memberships { organization_id, user_id, via: 'domain_match' } and lands them in the org /dashboard, audit-logged. Keep personal creates no membership. The prompt reappears on next login until they join, and never appears for users who already belong to that org.
Learners request, they don't auto-join¶
A learner who matches a verified domain sees "Your school uses Kwilo" with a Request to join button (POST /organization-join-requests). A trainer or admin at that org approves before membership is created. K-12 learners are never matched, always admin-provisioned.
Domain verification (org admin)¶
The org admin adds a domain in /settings/domains. Kwilo shows a TXT record (Host _kwilo-verify, Value kwilo-verify=<token>) to paste into DNS. Verify now does a DNS TXT lookup against _kwilo-verify.bit.edu.in; on a token match it sets verified_at = NOW(). Existing users on that domain trigger the join prompt on their next login. Domains are UNIQUE: a second org claiming the same domain gets "already verified by another organization, contact support".
Free-email and manual paths¶
Free-email domains can't be verified and return a 400 explaining to verify a custom Workspace domain instead. For white-glove onboarding where DNS access is slow, a platform admin marks a domain verified via /admin/domains (added_via = 'platform_admin_manual') with a required reason in the audit log.
Pending matches feed sales¶
Unmatched institutional signups accumulate in pending_domain_matches. The /admin/domains dashboard aggregates them by domain so BD can see which institutions have self-serve users but no deal yet.
Where it lives¶
/signup(signup-time match),/settings/domains(org admin),/admin/domains(platform admin)institutional_email_domains+pending_domain_matchestablesmatch_domain_and_queue_prompt()runs in the signup hooks (register_individual,verify_phone_otp,google_callback)FREE_EMAIL_DOMAINSconstant insrc/core/constants.py