@@ -488,6 +488,59 @@ const response = await fetch('http://localhost:3000/api/v1/auth/magic-link/send'
488488const response = await fetch (` http://localhost:3000/api/v1/auth/magic-link/verify?token=${token } ` );
489489```
490490
491+ ### Phone-Number Sign-In
492+
493+ Enable phone numbers as a first-class sign-in identifier (better-auth
494+ ` phone-number ` plugin). Opt-in — off by default:
495+
496+ ``` typescript
497+ new AuthPlugin ({
498+ secret: process .env .OS_AUTH_SECRET ,
499+ baseUrl: ' http://localhost:3000' ,
500+ plugins: {
501+ phoneNumber: true , // Enable phone sign-in
502+ }
503+ })
504+ ```
505+
506+ With the CLI ` serve ` command, ` OS_AUTH_PHONE_NUMBER_ENABLED=true ` flips the
507+ same flag. The plugin adds ` sys_user.phone_number ` (unique) and
508+ ` sys_user.phone_number_verified ` ; the login UI discovers the capability via
509+ ` features.phoneNumber ` on ` GET /api/v1/auth/config ` .
510+
511+ #### Sign In with Phone + Password
512+
513+ ``` typescript
514+ const response = await fetch (' http://localhost:3000/api/v1/auth/sign-in/phone-number' , {
515+ method: ' POST' ,
516+ headers: { ' Content-Type' : ' application/json' },
517+ body: JSON .stringify ({
518+ phoneNumber: ' +8613800000000' ,
519+ password: ' password123'
520+ })
521+ });
522+ ```
523+
524+ #### OTP Flows (Require an SMS Service)
525+
526+ The OTP surfaces — ` POST /phone-number/send-otp ` + ` POST /phone-number/verify `
527+ (sign-in / verification) and ` POST /phone-number/request-password-reset ` +
528+ ` POST /phone-number/reset-password ` (self-service reset) — only work when an
529+ SMS service is wired (` @objectstack/service-sms ` registers the ` sms ` kernel
530+ service). Without one, ` send-otp ` fails loudly instead of silently dropping
531+ the code; phone sign-in is then password-based only (an admin sets the
532+ password — see Admin User Management below). OTP requests are rate-limited
533+ per phone number to prevent SMS-pumping abuse.
534+
535+ #### Placeholder Emails for Phone-Only Accounts
536+
537+ better-auth requires a unique email per user, so accounts created with only a
538+ phone number get a generated placeholder address of the form
539+ ` u-<random>@placeholder.invalid ` . These are never deliverable (` .invalid ` is
540+ an RFC 2606 reserved TLD), never derived from the phone number (so exports
541+ and logs can't leak it), and every auth mail callback (password reset,
542+ invitation, magic link) refuses them with a ` PLACEHOLDER_EMAIL ` error.
543+
491544### Organizations (Multi-Tenant)
492545
493546Enable organization/team support:
@@ -502,6 +555,64 @@ new AuthPlugin({
502555})
503556```
504557
558+ ### Admin User Management
559+
560+ With ` plugins: { admin: true } ` (forced on when SCIM is enabled), platform
561+ admins get email-independent account management on the Users list — the
562+ ` create_user ` / ` set_user_password ` actions there drive these endpoints. All
563+ three routes are gated server-side on the platform-admin signals
564+ (` isPlatformAdmin ` , the ` platform_admin ` position, or the legacy ` role `
565+ scalar) and drive the better-auth pipeline, so created accounts get a real
566+ scrypt-hashed credential and can sign in immediately.
567+
568+ #### Create a User Directly
569+
570+ ``` typescript
571+ const response = await fetch (' http://localhost:3000/api/v1/auth/admin/create-user' , {
572+ method: ' POST' ,
573+ headers: { ' Content-Type' : ' application/json' },
574+ body: JSON .stringify ({
575+ email: ' new.user@example.com' , // and/or phoneNumber (requires plugins.phoneNumber)
576+ name: ' New User' ,
577+ generatePassword: true , // or pass an explicit `password`
578+ mustChangePassword: true // default true
579+ })
580+ });
581+ // → { success, data: { user, temporaryPassword? } }
582+ ```
583+
584+ A generated temporary password is returned ** once** in the response (the
585+ Users-list action reveals it in a one-shot dialog) and is never persisted or
586+ logged. While ` mustChangePassword ` is set, the user is blocked with
587+ ` 403 PASSWORD_EXPIRED ` on every protected route until they complete a
588+ password change — the same gate and Console redirect as password expiry.
589+
590+ #### Set / Reset a Password
591+
592+ ` POST /api/v1/auth/admin/set-user-password ` accepts
593+ ` { userId, newPassword } ` or ` { userId, generatePassword: true } ` plus the
594+ same ` mustChangePassword ` flag. It also provisions a credential account for
595+ SSO-onboarded users who never had a local password.
596+
597+ #### Bulk Import Users
598+
599+ ` POST /api/v1/auth/admin/import-users ` accepts the same payload shapes as the
600+ generic data import (` rows[] ` / ` csv ` / ` xlsxBase64 ` , ` dryRun ` ), but writes
601+ every row through better-auth so the accounts are login-capable:
602+
603+ - ` passwordPolicy: 'invite' ` — rows need a reachable identity: a real email
604+ (invitation goes out as a set-your-password email; requires an email
605+ service) or, for phone-only rows, a wired SMS service (invitation SMS +
606+ phone-OTP first sign-in).
607+ - ` passwordPolicy: 'temporary' ` — per-row generated temporary passwords,
608+ returned once in the response, with ` mustChangePassword ` enforced.
609+ - ` mode: 'insert' | 'upsert' ` with ` matchBy: 'email' | 'phone' ` . Upsert
610+ updates only touch profile fields (` name ` , ` phone_number ` , ` role ` ) — a
611+ re-imported file can never modify an existing user's email or reset their
612+ password.
613+ - Synchronous only, at most 500 rows per request (password hashing is
614+ CPU-bound); split larger files into batches. Imports are not undoable.
615+
505616---
506617
507618## Client Integration
@@ -613,6 +724,21 @@ All endpoints are available under `/api/v1/auth/*`:
613724- ` POST /api/v1/auth/magic-link/send ` - Send magic link email
614725- ` GET /api/v1/auth/magic-link/verify ` - Verify magic link
615726
727+ #### Phone Number (requires ` plugins.phoneNumber ` )
728+
729+ - ` POST /api/v1/auth/sign-in/phone-number ` - Sign in with phone + password
730+ - ` POST /api/v1/auth/phone-number/send-otp ` - Send a sign-in/verification OTP (requires an SMS service; rate-limited)
731+ - ` POST /api/v1/auth/phone-number/verify ` - Verify an OTP
732+ - ` POST /api/v1/auth/phone-number/request-password-reset ` - Request an OTP-based password reset (requires an SMS service)
733+ - ` POST /api/v1/auth/phone-number/reset-password ` - Complete an OTP-based password reset
734+
735+ #### Admin User Management (requires ` plugins.admin ` ; platform-admin gated)
736+
737+ - ` POST /api/v1/auth/admin/create-user ` - Create a login-capable account (optional one-time temporary password + forced change)
738+ - ` POST /api/v1/auth/admin/set-user-password ` - Set/reset a user's password (also provisions a credential for SSO-onboarded users)
739+ - ` POST /api/v1/auth/admin/import-users ` - Bulk import users (CSV/JSON/XLSX; invite or temporary password policy; ≤500 rows, dry-run supported)
740+ - ` POST /api/v1/auth/admin/unlock-user ` - Clear a brute-force lockout early
741+
616742For complete API documentation, see the [ Better-Auth API Reference] ( https://www.better-auth.com/docs ) .
617743
618744---
0 commit comments