Onboarding journey
The user-facing path from “first launch” to “logged in on the home dashboard”: signup → verify email → first login (with inline FCM + timezone capture) → dashboard load. The auth subsystem that backs every step (JWT, refresh, password reset, token storage) is documented separately at Auth subsystem.
Journey at a glance
Every transition is driven by AuthViewModel collecting AuthState in shared/commonMain. The same screens render on Android, iOS, and desktop — no per-platform onboarding code.
What happens at each step
1. Signup
The user submits name, email, password. The client sends POST /api/v1/auth/register (gated by the SIGN_UP feature flag per X-Platform — disabled platforms see a 403 with FEATURE_DISABLED). On success the server inserts the user with email_verified_at = null and immediately fires a 6-digit PIN to their email via EmailVerificationService.
The UI transitions to Email-pending:
- Shows the email the PIN was sent to (so the user can spot typos).
- “Resend” button calls
POST /api/v1/auth/request-verification-email(rate-limited underforgot— 5/10 min per IP). - “Wrong email?” → back to signup.
2. Email verification
The user enters the 6-digit PIN. POST /api/v1/auth/verify-email {email, code} flips Users.emailVerifiedAt to now. The UI auto-advances to the Login screen (or directly to login-and-land if the password is still in memory — see note below).
Login attempts before verification return 403 EMAIL_NOT_VERIFIED, which AuthViewModel maps to Unauthenticated(pendingVerificationEmail = email) so the UI can route back to the Email-pending screen. This is the recovery path for users who close the app before verifying.
3. First login
POST /api/v1/auth/login is a single transaction that does three things:
- Verifies credentials → returns
AuthResponse{token, refreshToken, tier}. - Captures the device’s FCM token if the client passed
fcmTokenin the body — the server callstimerService.registerDeviceToken(userId, fcmToken, "android"). ⚠️ The platform string is hardcoded to"android"inAuthRouting.kt; iOS / desktop clients must follow up withPOST /api/v1/auth/device-tokenand the correct platform after login. - Captures the device’s IANA timezone if the client passed
timezone— server validates and updatesUsers.timeZone(silent if invalid).
On success the client stores both tokens in TokenStorage (per-platform DataStore on Android/iOS/desktop, localStorage on web targets) and routes to the dashboard.
4. Dashboard load
Immediately after Authenticated(tier), the dashboard view-model fires:
The event bus decouples the dashboard refresh from the action that triggered it — no explicit callback wiring needed between features.
Password reset (user-facing)
Three screens. The server mechanics (PIN hashing, opaque session token, refresh-session sweep) are in Auth subsystem → Password reset.
- Forgot password — user enters email →
POST /auth/forgot-password. Server always returns 200 (anti-enumeration). UI advances unconditionally. - PIN entry — user receives a 6-digit code valid for 10 minutes, 5 attempts. On success →
POST /auth/reset-password/verifyreturns a short-lived reset session token the UI holds in memory only. - New password — UI submits
POST /auth/reset-password {resetToken, newPassword}. Server hashes the new password, marks the session used, and revokes every active refresh session for the user. The UI routes back to login.
Returning user path
On every app launch AuthViewModel.init dispatches CheckAuth:
TokenStorage.getToken()is non-null →AuthService.fetchMe()validates againstGET /api/v1/auth/me. If 200, state →Authenticated(tier)and the dashboard loads. If 401, the HTTP client validator tries/auth/refreshonce; success keeps the user signed in transparently, failure broadcastsSessionExpiredNotifier→ state →Unauthenticated(errorMessage="Your session expired…").TokenStorage.getToken()is null → state →Unauthenticated. Login screen.
The refresh token persists across app restart and process kill in the same DataStore/localStorage slot as the access token; the user only re-enters credentials when the refresh chain is revoked (logout, password reset, replay-detection, refresh expiry after 30 days).
Logout
User taps Logout in settings:
- Client calls
AuthService.logout():- Fires
POST /api/v1/auth/logoutwith the stored refresh token (best-effort; network errors are swallowed). - Clears both
auth_tokenandrefresh_tokenfromTokenStorage.
- Fires
AuthStatetransitions toUnauthenticatedand the UI returns to the login screen.
Server-side, the supplied refresh token is marked revoked_at = now so any leaked copy stops working immediately. Other devices the same user is logged in on are not affected — those have their own refresh sessions.
Client building blocks
| Module | Role |
|---|---|
AuthViewModel | Single source of AuthState; collects SessionExpiredNotifier; reacts to AuthIntents |
AuthState | Loading · Authenticated(tier) · Unauthenticated(email, password, name, errorMessage, isLoading, pendingVerificationEmail) |
AuthIntent | CheckAuth · Login · Register · Logout · SessionExpired |
AuthService | HTTP client for all /auth/* endpoints; persists tokens via TokenStorage; returns ApiOutcome<T> |
Troubleshooting (onboarding-specific)
“I entered the right PIN but verify-email rejects it”
- PIN expires after 10 minutes. Tap Resend (rate-limited 5/10 min per IP).
- 5 wrong attempts on the same PIN locks it — new PIN required.
“I never received the verification email”
- Check spam / promotional inboxes. SES sender is
noreply@<your-domain>. - For SES domain verification issues, see FCM troubleshooting (same SES infrastructure).
“After verify-email, login still says EMAIL_NOT_VERIFIED”
- Stale 403 cached in
AuthState.errorMessage. The next manual login attempt clears it.
“Logged in on desktop, not getting push notifications”
- Desktop never sends an FCM token. Push is mobile-only by design today. See Notifications for the device-token wiring and platform support matrix.
Related docs
- Auth subsystem — JWT, refresh, password reset internals, token storage, 401 handling
- Refresh tokens & key rotation — rotation rules, replay detection, env vars
- Notifications — what happens with the FCM token captured at login
- Dashboard — the screen the user lands on after first login