You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Login page: support phone number + password sign-in #2416
The Console login page currently supports email + password and phone + verification code (OTP), but not phone number + password. The backend already implements it — this is a frontend/SDK gap.
better-auth's phoneNumber plugin exposes POST /sign-in/phone-number ({ phoneNumber, password } → session). It's registered whenever the phoneNumber plugin is enabled.
The platform does not set requireVerification, so an unverified phone can still sign in with a password (once a password is set).
Unlike OTP, phone+password needs no SMS service.
The capability is already advertised: getPublicConfig().features.phoneNumber = "phoneNumber plugin enabled" (distinct from features.phoneNumberOtp, which also requires a deliverable SMS service). The objectui type AuthPublicConfig.features.phoneNumber already exists too.
Frontend (objectui) — the gap:
LoginForm has mode: 'password' | 'phone-otp'. The password mode's field is Email only and submits via signIn(email, password) → /sign-in/email. There is no path to /sign-in/phone-number.
createAuthClient exposes signInWithPhoneOtp but no signInWithPhonePassword.
Critical correctness note — phone normalization:
The backend's normalizePhoneNumber just strips [\s\-().] and validates ^\+?[0-9]{6,15}$ — it does not force E.164 or inject a country code. Accounts are stored in that light-stripped form. The login form therefore must normalize identically (strip the same characters, send as-is); adding a country code or full E.164 conversion would break lookups for numbers stored without one. So no libphonenumber dependency is needed — and would in fact be wrong here.
How mainstream apps handle it
Unified identifier field ("Email or phone", auto-route by format) — Google, X, Facebook, LinkedIn, Amazon. Simplest UX.
Segmented / tabbed (Email | Phone, often with a country-code selector) — WeChat, Alipay, Taobao, Meituan, DiDi. Phone-first, common in China.
Chosen approach: unified identifier field in the existing password mode — minimal UI change, coexists with the phone-OTP mode, and matches the dominant global pattern.
Wire it through AuthClient (types), AuthContext, AuthProvider, useAuth default.
Shared normalizePhoneIdentifier helper mirroring the backend (strip [\s\-().], validate ^\+?[0-9]{6,15}$).
LoginForm: in password mode, accept an email-or-phone identifier gated on features.phoneNumber; auto-route to /sign-in/email vs /sign-in/phone-number by format. Update the field label/type; keep SSO routing email-only.
Tests: identifier normalization/detection, client method, and LoginForm routing.
Out of scope
Country-code selector / E.164 migration (backend stores non-E.164; would be a separate, coordinated change).
Phone + password self-registration (signUpOnVerification is deliberately off; accounts are provisioned by admin/import).
Notes
Only works for accounts that have a phone number and a password set. Phone-only accounts created via import use the invite policy: first sign-in via OTP, then set a password — after which phone+password works.
Activity
feat(auth): phone number + password sign-in on the login page (#2418)