- email ownership is verified before password setup
- user activation is explicit (
pending→active) - no secrets (OTP or magic link) can be reused
- responses are fully localized from the start of the request
- session tokens are issued only after activation
Registration strategies
Flowxi supports two onboarding strategies, both leading to the same final state.- Magic link registration
- Email code (OTP) registration
- user created (or reused) as
status: pending - locale captured as early as possible (
user.localeis set if empty) - notification preferences created automatically
- account becomes
activeonly when password is set - token is returned only at the activation step
Common behavior (applies to both strategies)
Locale resolution
Before any controller logic runs, the locale is resolved and stored aseffective_locale.
Priority:
X-App-LocaleAccept-Languageuser.locale(only when authenticated, not typical for register)- fallback
fr
app()->setLocale($lang)- emails are sent with
Mail::to(...)->locale($lang) - if the user exists and has no locale yet,
user.locale = $lang
code, never on translated message.
User creation / reuse rules
When a registration request comes in:- if the user does not exist:
- user is created with:
emailstatus = pendingname = nulllocale = $lang
usernameis generated and persisted
- user is created with:
- if the user exists but is not active:
- missing
localeand/orusernameare filled
- missing
- if the user exists and is already
active:- the flow is blocked with:
code: EMAIL_ALREADY_USED(send/register)- or
code: EMAIL_ALREADY_ACTIVE(resend)
- the flow is blocked with:
Notification preferences auto-created
During registration, Flowxi ensures a baseline notification config exists:email_enabled: trueinapp_enabled: truepush_enabled: false
Activation is always the same
Regardless of strategy, the activation step does:password = Hash::make(password)status = activeemail_verified_at = now()if empty- token creation (
Sanctum::createToken(...)) - returns
access_token,token_type,user_id,account_status
Strategy 1: Magic link registration
Flow summary
- Client calls send magic link
- User receives email with link (deep link + open URL)
- Client opens
/register/set-passwordscreen (frontend) - Client calls set password with
token + email + password - Account becomes active and a Bearer token is returned
1) Send magic link
EndpointPOST /api/v1/auth/register-email
Request
Required:
email(string, email, max 255)
- DB transaction +
lockForUpdate()on the user row (if exists) - blocks active accounts:
-
creates or updates
pendinguser (locale/username filled if missing) - assigns a default avatar if empty (Dicebear URL seeded by user id)
-
invalidates any previous unused magic links (
used_at = now()) -
creates a new magic link token:
expires_at = now() + 15 minutesused_at = null
-
sends localized email containing:
rawUrl(direct deep link)actionUrl(open redirect URL wrapper)
2) Resend magic link
EndpointPOST /api/v1/register-email/resend
Request
Required:
email(string, email, max 255)
- DB transaction +
lockForUpdate()on user - if user not found:
- if already active:
- invalidates any previous unused links
- generates a fresh token (15 min TTL)
- sends localized magic link email
3) Set password (activate via magic link)
EndpointPOST /api/v1/auth/register/set-password
Request
Required:
email(string)token(string)password(string) — validated bySetPasswordRequest
-
locks magic link row and validates token:
- token exists
used_at IS NULLexpires_at > now()
- if token invalid/expired/used:
- locks user row
- email mismatch returns the same
MAGIC_LINK_INVALID(anti-enumeration) - sets password + activates the account
- marks magic link as used (
used_at = now()) - creates Sanctum token (
register:set-password)
Strategy 2: Email code (OTP) registration
Flow summary
- Client calls send code
- User receives a 6-digit code
- Client calls verify code (optional but recommended UX step)
- Client calls set password with
email + code + password - Account becomes active and a Bearer token is returned
app.key.
1) Send email code (OTP)
EndpointPOST /api/v1/register-email-code/send
Request
Validated by SendEmailCodeRequest (email required, etc).
Rate limit
Keyed by reg:otp:send:{email}:{ip}Max: 5 attempts / 600s On limit exceeded:
- email normalized (lowercase + trim)
- blocks active accounts:
-
creates or updates
pendinguser (fills locale/username if missing) - creates notification preferences if missing
- generates OTP: random 6-digit
- deletes any previous OTP rows for the email
-
inserts one row into
email_verification_codes:code_hash = hash_hmac('sha256', sprintf('%06d', code), app.key)expires_at = now() + 10 minutes
2) Resend OTP
EndpointPOST /api/v1/register-email-code/resend
Rate limit
Keyed by reg:otp:resend:{email}:{ip}Max: 5 attempts / 600s Behavior
- if user not found:
- if already active:
- regenerates OTP exactly like
/send
3) Verify OTP
EndpointPOST /api/v1/register-email-code/verify
Rate limit
Keyed by reg:otp:verify:{email}:{ip}Max: 10 attempts / 900s Request Validated by
VerifyEmailCodeRequest:
emailcode(6 digits; server normalizes to digits only)
-
loads row where:
email = ...expires_at > now()
- if missing/expired:
- hashes the provided code and compares via
hash_equals - if mismatch:
4) Set password (activate via OTP)
EndpointPOST /api/v1/register-email-code/set-password
Rate limit
Keyed by reg:otp:setpwd:{email_prefix}:{ip} (email prefix is truncated)Max: 20 attempts / 900s Request Validated by
SetPasswordFromCodeRequest:
emailcodepassword
-
locks OTP row (
email_verification_codes) and validates:- not expired
- hash matches
- on failure returns 403 (stronger than verify):
- locks user row and validates existence (also returns
OTP_INVALIDif missing) - sets password + activates (
status = active,email_verified_at) - deletes OTP row
- creates Sanctum token (
register:set-password-from-code)
Security and anti-enumeration notes
What is intentionally revealed
Registration does reveal some state in specific endpoints:/register-email-code/resendreturnsUSER_NOT_FOUNDif the email is not registered as pending- magic-link resend does the same
What is intentionally not leaked
-
magic link set-password returns the same
MAGIC_LINK_INVALIDfor:- invalid token
- used token
- expired token
- email mismatch
Frontend integration checklist
-
Always send
X-App-Localefor consistent language + email locale -
OTP flow:
- call
/send - optionally call
/verifyto show “code ok” - call
/set-passwordto activate and obtain token
- call
-
magic link flow:
- call
/auth/register-email - open frontend route
/register/set-passwordwithtoken + email + lang - submit
/auth/register/set-passwordto activate and obtain token
- call
-
Always branch on
code, notmessage -
Handle
RATE_LIMITED (429)with cooldown UI
Summary
Flowxi registration provides:- deterministic
pending → activeactivation - secure OTP hashing (HMAC with app key) and DB-enforced expiration
- single-use expiring magic links (15 minutes, consumed on success)
- localized responses and emails aligned per request locale
- token issuance only at the final activation step

