Authentication-nexgate-service(1) NextGate Authentication V2 (DEPRECATED) Author : Josh Backend Team Last Updated : 2025-01-24 Version : v1.0 Base URL : https://api.nextgate.com/api/v1 Short Description : NextGate Authentication API provides a comprehensive, passwordless-first authentication system with OAuth2 support (Google, Apple), multi-channel OTP verification, device tracking, session management, and a 6-step user onboarding flow. Designed for mobile-first applications with security features including risk assessment, rate limiting, and refresh token rotation. Hints : ๐Ÿ” Passwordless by Default : Users can authenticate via OTP (SMS/Email) without ever setting a password ๐Ÿ“ฑ Device Trust : Each device is tracked and can be managed by the user ๐Ÿ”„ Token Rotation : Refresh tokens are rotated on each use for enhanced security โฑ๏ธ OTP Validity : All OTP codes expire in 10 minutes with max 3 verification attempts ๐ŸŒ Multi-Channel : OTP can be sent via Email or SMS based on user preference ๐Ÿ“Š Metadata System : Onboarding responses include nextStepMetadata for pre-filling forms Authentication Architecture Overview System Design Philosophy NextGate Authentication is built on three core principles: Passwordless-First : Users authenticate primarily via OTP, with password as an optional secondary method Device-Aware Security : Every login tracks device information for risk assessment Progressive Onboarding : New users complete a 6-step guided setup process Authentication Flow Diagram โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ NEXTGATE AUTHENTICATION FLOW โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ START โ”‚ โ”‚ /auth/start โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ PHONE โ”‚ โ”‚ EMAIL โ”‚ โ”‚ USERNAME โ”‚ โ”‚ +255... โ”‚ โ”‚ @email.comโ”‚ โ”‚ @handle โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ–ผ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ NEW USER โ”‚ โ”‚ EXISTING USER โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข Send OTP โ”‚ โ”‚ Has Password? โ”‚ โ”‚ โ€ข Create โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ Account โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ PASSWORD โ”‚ โ”‚ PASSWORDLESSโ”‚ โ”‚ โ”‚ LOGIN โ”‚ โ”‚ (OTP SENT) โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ–ผ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ VERIFY OTP โ”‚ โ”‚ DEVICE CHECK โ”‚ โ”‚ /auth/verify โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ Known Device? โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ KNOWN โ”‚ โ”‚ NEW โ”‚ โ”‚ โ”‚ DEVICE โ”‚ โ”‚ DEVICE โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ†’ Login OK โ”‚ โ”‚ โ†’ Verify โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ via OTP โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ–ผ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ OTP VERIFIED โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ–ผ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ONBOARDING โ”‚ โ”‚ FULLY โ”‚ โ”‚ REQUIRED โ”‚ โ”‚ AUTHENTICATED โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (New User or โ”‚ โ”‚ โ†’ Access Token โ”‚ โ”‚ Incomplete) โ”‚ โ”‚ โ†’ Refresh Token โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 6-STEP ONBOARDING โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ 1. โ”‚โ”€โ”€โ–ถโ”‚ 2. โ”‚โ”€โ”€โ–ถโ”‚ 3. โ”‚โ”€โ”€โ–ถโ”‚ 4. โ”‚ โ”‚ โ”‚ โ”‚ OTP โ”‚ โ”‚ AGE โ”‚ โ”‚USER โ”‚ โ”‚INT- โ”‚ โ”‚ โ”‚ โ”‚VRFY โ”‚ โ”‚+NAMEโ”‚ โ”‚NAME โ”‚ โ”‚ESTS โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ 5. โ”‚ โ”‚ โ”‚ โ”‚PRFL โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”ฌโ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ COMPLETE โ”‚ โ”‚ โ”‚ โ”‚ โ†’ Access Token โ”‚ โ”‚ โ†’ Refresh Token โ”‚ โ”‚ โ†’ Welcome! โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Token Types Token Purpose Expiry Storage tempToken OTP verification, flow state 10 min Memory only onboardingToken Onboarding step progression 30 min Memory only onboardingRefreshToken Refresh onboarding token 24 hours Secure storage accessToken API authentication 1 hour Secure storage refreshToken Get new access tokens 30 days Secure storage deviceVerificationToken New device verification 10 min Memory only Onboarding Steps Explained Step Enum Value What Happens Metadata Provided 1 INITIATED Account created, OTP sent - 2 OTP_VERIFIED OTP confirmed, ready for age firstName , lastName , profilePicture (from OAuth) 3 AGE_VERIFIED Age confirmed, names saved suggestedUsernames (5 smart suggestions) 4 USERNAME_SET Username chosen - 5 INTERESTS_SELECTED Interests saved firstName , lastName , username , suggestedDisplayName , profilePicture 6 COMPLETED Profile complete, tokens issued - Device Fingerprinting Guide Why Device Fingerprinting? Device fingerprinting creates a unique identifier for each device/browser combination. This enables: Security : Detect new/unknown devices attempting login Trust Levels : Known devices can skip additional verification Session Management : Users can see and revoke device access Implementation for Frontend Recommended Library: FingerprintJS npm install @fingerprintjs/fingerprintjs React Implementation // hooks/useDeviceFingerprint.js import { useState, useEffect } from 'react'; import FingerprintJS from '@fingerprintjs/fingerprintjs'; export const useDeviceFingerprint = () => { const [fingerprint, setFingerprint] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const generateFingerprint = async () => { try { const fp = await FingerprintJS.load(); const result = await fp.get(); setFingerprint({ deviceId: result.visitorId, confidence: result.confidence.score, components: { platform: result.components.platform?.value, timezone: result.components.timezone?.value, language: result.components.languages?.value?.[0], screenResolution: result.components.screenResolution?.value, colorDepth: result.components.colorDepth?.value, } }); } catch (error) { console.error('Fingerprint generation failed:', error); // Fallback: Generate a random UUID and store in localStorage let fallbackId = localStorage.getItem('ng_device_id'); if (!fallbackId) { fallbackId = crypto.randomUUID(); localStorage.setItem('ng_device_id', fallbackId); } setFingerprint({ deviceId: fallbackId, confidence: 0.5 }); } finally { setLoading(false); } }; generateFingerprint(); }, []); return { fingerprint, loading }; }; Usage in Authentication // components/LoginForm.jsx import { useDeviceFingerprint } from '../hooks/useDeviceFingerprint'; const LoginForm = () => { const { fingerprint, loading } = useDeviceFingerprint(); const handleStartAuth = async (identifier) => { if (loading || !fingerprint) { console.warn('Device fingerprint not ready'); return; } const response = await fetch('/api/v1/auth/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: identifier, deviceId: fingerprint.deviceId, deviceName: getDeviceName(), platform: getPlatform(), }), }); const data = await response.json(); // Handle response... }; return (/* Your form JSX */); }; // Helper: Generate human-readable device name const getDeviceName = () => { const ua = navigator.userAgent; let browser = 'Unknown Browser'; let os = 'Unknown OS'; if (ua.includes('Chrome')) browser = 'Chrome'; else if (ua.includes('Firefox')) browser = 'Firefox'; else if (ua.includes('Safari')) browser = 'Safari'; else if (ua.includes('Edge')) browser = 'Edge'; if (ua.includes('Windows')) os = 'Windows'; else if (ua.includes('Mac')) os = 'macOS'; else if (ua.includes('Linux')) os = 'Linux'; else if (ua.includes('Android')) os = 'Android'; else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS'; return `${browser} on ${os}`; }; // Helper: Get platform type const getPlatform = () => { const ua = navigator.userAgent; if (ua.includes('Android')) return 'ANDROID'; if (ua.includes('iPhone') || ua.includes('iPad')) return 'IOS'; return 'WEB'; }; Alternative: Custom Fingerprint (No Library) // utils/customFingerprint.js export const generateCustomFingerprint = async () => { const components = []; // Screen properties components.push(window.screen.width); components.push(window.screen.height); components.push(window.screen.colorDepth); components.push(window.devicePixelRatio); // Timezone & Language components.push(Intl.DateTimeFormat().resolvedOptions().timeZone); components.push(navigator.language); components.push(navigator.platform); components.push(navigator.hardwareConcurrency || 'unknown'); // Canvas fingerprint try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.textBaseline = 'top'; ctx.font = '14px Arial'; ctx.fillText('NextGate Device FP', 2, 2); components.push(canvas.toDataURL()); } catch (e) { components.push('canvas-blocked'); } // WebGL renderer try { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl'); const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); if (debugInfo) { components.push(gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)); } } catch (e) { components.push('webgl-blocked'); } // Create hash const fingerprint = components.join('|||'); const encoder = new TextEncoder(); const data = encoder.encode(fingerprint); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); }; Required Headers for All Auth Requests const authHeaders = { 'Content-Type': 'application/json', 'X-Device-Id': fingerprint.deviceId, 'X-Device-Name': getDeviceName(), 'X-Platform': getPlatform(), }; // For authenticated requests: authHeaders['Authorization'] = `Bearer ${accessToken}`; // For session management: authHeaders['X-Session-Id'] = sessionId; โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ COMPLETE ONBOARDING FLOW โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ STEP 0: ENTRY POINT โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Two Entry Points โ”‚ โ”‚ โ”‚ โ”‚ A) Phone/Email Login B) OAuth (Google/Apple) โ”‚ โ”‚ POST /auth/start POST /auth/login/oauth โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ OTP Sent to User Google Validates User โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ POST /auth/verify โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ Response contains: โ”‚ โ”‚ โ€ข onboardingToken โ”‚ โ”‚ โ€ข currentStep: INITIATED โ”‚ โ”‚ โ€ข nextStep: AGE_VERIFIED โ”‚ โ”‚ โ€ข nextStepMetadata: { โ”‚ โ”‚ firstName: "John", // from OAuth โ”‚ โ”‚ lastName: "Doe", // from OAuth โ”‚ โ”‚ profilePicture: "..." // from OAuth โ”‚ โ”‚ hasOAuthData: true โ”‚ โ”‚ } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ STEP 1: AGE & NAME VERIFICATION โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• POST /auth/onboarding/age โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ UI: โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ๐Ÿ“… Let's verify your age โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ First Name: [John____________] <- Pre-filled OAuth โ”‚ โ”‚ โ”‚ โ”‚ Last Name: [Doe_____________] <- Pre-filled OAuth โ”‚ โ”‚ โ”‚ โ”‚ Birth Date: [____/____/______] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [Continue โ†’] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ REQUEST: โ”‚ โ”‚ { โ”‚ โ”‚ "onboardingToken": "eyJ...", โ”‚ โ”‚ "firstName": "John", โ”‚ โ”‚ "lastName": "Doe", โ”‚ โ”‚ "birthDate": "1999-05-15" โ”‚ โ”‚ } โ”‚ โ”‚ โ”‚ โ”‚ RESPONSE: โ”‚ โ”‚ { โ”‚ โ”‚ "onboardingToken": "eyJ...", โ”‚ โ”‚ "accountTier": "FULL", โ”‚ โ”‚ "age": 25, โ”‚ โ”‚ "currentStep": "AGE_VERIFIED", โ”‚ โ”‚ "nextStep": "USERNAME_SET", โ”‚ โ”‚ "nextStepMetadata": { โ”‚ โ”‚ "suggestedUsernames": [ โ”‚ โ”‚ "johndoe99", "john_doe_official", "jdoe_tz" โ”‚ โ”‚ ] โ”‚ โ”‚ } โ”‚ โ”‚ } โ”‚ โ”‚ โ”‚ โ”‚ โš ๏ธ age < 13 -> blocked, account deleted โ”‚ โ”‚ โš ๏ธ age 13-17 -> accountTier = RESTRICTED โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ STEP 2: USERNAME SELECTION โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• POST /auth/onboarding/username POST /auth/onboarding/check-username (real-time availability) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ UI: โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ๐Ÿ‘ค Choose your username โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ @[________________] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Suggestions: โ”‚ โ”‚ โ”‚ โ”‚ [ johndoe99 ] [ john_doe ] [ jdoe_tz ] โ”‚ โ”‚ โ”‚ โ”‚ [ the_johndoe ] [ johnd_pro ] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [Continue โ†’] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ RESPONSE: โ”‚ โ”‚ { โ”‚ โ”‚ "onboardingToken": "eyJ...", โ”‚ โ”‚ "username": "johndoe99", โ”‚ โ”‚ "currentStep": "USERNAME_SET", โ”‚ โ”‚ "nextStep": "CONTACT_VERIFIED", <-- NEW โ”‚ โ”‚ "nextStepMetadata": { โ”‚ โ”‚ "contactVerification": { โ”‚ โ”‚ "requiredContactType": "EMAIL", โ”‚ โ”‚ "requiresInput": true, โ”‚ โ”‚ "alreadyVerified": { โ”‚ โ”‚ "type": "PHONE", โ”‚ โ”‚ "maskedValue": "... ... ..50" โ”‚ โ”‚ }, โ”‚ โ”‚ "reasons": [ โ”‚ โ”‚ "TICKET_DELIVERY", โ”‚ โ”‚ "ACCOUNT_RECOVERY", โ”‚ โ”‚ "ACCOUNT_SECURITY" โ”‚ โ”‚ ] โ”‚ โ”‚ } โ”‚ โ”‚ } โ”‚ โ”‚ } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ STEP 3: CONTACT VERIFICATION <-- NEW STEP โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• POST /auth/onboarding/contact/initiate POST /auth/onboarding/contact/verify POST /auth/onboarding/contact/edit (inline, no page nav) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ UI (single page, three inline states): โ”‚ โ”‚ โ”‚ โ”‚ STATE A โ€” Input (if requiresInput = true): โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ๐Ÿ“ง Add your email address โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Your phone ... ...50 is already verified โœ… โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ We need your email for: โ”‚ โ”‚ โ”‚ โ”‚ โ€ข Ticket delivery โ”‚ โ”‚ โ”‚ โ”‚ โ€ข Account recovery โ”‚ โ”‚ โ”‚ โ”‚ โ€ข Security alerts โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Email: [________________________] โ”‚ โ”‚ โ”‚ โ”‚ [Send Code โ†’] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ INITIATE REQUEST: โ”‚ โ”‚ { โ”‚ โ”‚ "onboardingToken": "eyJ...", โ”‚ โ”‚ "contactValue": "john@gmail.com" โ”‚ โ”‚ } โ”‚ โ”‚ โ”‚ โ”‚ INITIATE RESPONSE: โ”‚ โ”‚ { โ”‚ โ”‚ "tempToken": "eyJ...", โ”‚ โ”‚ "onboardingToken": "eyJ...", โ”‚ โ”‚ "maskedValue": "j......@g.....com", โ”‚ โ”‚ "contactType": "EMAIL", โ”‚ โ”‚ "expiresIn": 600 โ”‚ โ”‚ } โ”‚ โ”‚ โ”‚ โ”‚ STATE B โ€” OTP Input: โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Enter the code sent to j......@g.....com โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [_] [_] [_] [_] [_] [_] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [Edit email] ยท Resend (60s cooldown) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ VERIFY REQUEST: โ”‚ โ”‚ { โ”‚ โ”‚ "onboardingToken": "eyJ...", โ”‚ โ”‚ "tempToken": "eyJ...", โ”‚ โ”‚ "otp": "847291" โ”‚ โ”‚ } โ”‚ โ”‚ โ”‚ โ”‚ VERIFY RESPONSE: โ”‚ โ”‚ { โ”‚ โ”‚ "onboardingToken": "eyJ...", โ”‚ โ”‚ "currentStep": "CONTACT_VERIFIED", โ”‚ โ”‚ "nextStep": "INTERESTS_SELECTED", โ”‚ โ”‚ "verified": true โ”‚ โ”‚ } โ”‚ โ”‚ โ”‚ โ”‚ STATE C โ€” Edit (tapping [Edit email] transforms inline): โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ New email: [________________________] โ”‚ โ”‚ โ”‚ โ”‚ [Send new code โ†’] [Cancel] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ EDIT REQUEST: โ”‚ โ”‚ { โ”‚ โ”‚ "onboardingToken": "eyJ...", โ”‚ โ”‚ "tempToken": "eyJ...", โ”‚ โ”‚ "newContactValue": "other@gmail.com" โ”‚ โ”‚ } โ”‚ โ”‚ -> Returns new tempToken + new maskedValue โ”‚ โ”‚ -> Previous OTP invalidated immediately โ”‚ โ”‚ โ”‚ โ”‚ requiredContactType by signup method: โ”‚ โ”‚ โ€ข Phone user -> verify EMAIL โ”‚ โ”‚ โ€ข Email user -> verify PHONE โ”‚ โ”‚ โ€ข Google/Apple -> verify PHONE (email via OAuth already) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ STEP 4: INTERESTS SELECTION โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• GET /interests/categories/all POST /auth/onboarding/interests โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ UI: โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ๐ŸŽฏ What are you interested in? (select at least 3) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [๐ŸŽต Music] [โšฝ Sports] [๐ŸŽฎ Gaming] [๐Ÿ“ฑ Tech] โ”‚ โ”‚ โ”‚ โ”‚ [๐ŸŽฌ Movies] [๐Ÿ“š Books] [๐Ÿ• Food] [โœˆ๏ธ Travel] โ”‚ โ”‚ โ”‚ โ”‚ ... more โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [Continue โ†’] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ RESPONSE: โ”‚ โ”‚ { โ”‚ โ”‚ "onboardingToken": "eyJ...", โ”‚ โ”‚ "selectedInterests": ["Music", "Tech", "Travel"], โ”‚ โ”‚ "count": 3, โ”‚ โ”‚ "currentStep": "INTERESTS_SELECTED", โ”‚ โ”‚ "nextStep": "PROFILE_COMPLETED", โ”‚ โ”‚ "nextStepMetadata": { โ”‚ โ”‚ "firstName": "John", โ”‚ โ”‚ "lastName": "Doe", โ”‚ โ”‚ "username": "johndoe99", โ”‚ โ”‚ "suggestedDisplayName": "John Doe", โ”‚ โ”‚ "profilePicture": "https://..." โ”‚ โ”‚ } โ”‚ โ”‚ } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ STEP 5: PROFILE COMPLETION (FINAL) โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• POST /auth/onboarding/profile โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ UI: โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โœจ Complete your profile โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [ ๐Ÿ“ท photo ] <- Pre-filled from OAuth โ”‚ โ”‚ โ”‚ โ”‚ or upload new โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Display Name: [John Doe________] <- Pre-filled โ”‚ โ”‚ โ”‚ โ”‚ @johndoe99 (locked here, change in settings) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Bio: [________________________________] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [๐ŸŽ‰ Complete Setup] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ HEADERS: โ”‚ โ”‚ X-Device-Id: "abc123fingerprint" โ”‚ โ”‚ X-Device-Name: "Chrome on macOS" โ”‚ โ”‚ X-Platform: "WEB" โ”‚ โ”‚ โ”‚ โ”‚ REQUEST: โ”‚ โ”‚ { โ”‚ โ”‚ "onboardingToken": "eyJ...", โ”‚ โ”‚ "displayName": "John Doe", โ”‚ โ”‚ "bio": "Tech enthusiast | Based in Dar es Salaam", โ”‚ โ”‚ "profilePictureUrl": "https://storage.nextgate.com/..." โ”‚ โ”‚ } โ”‚ โ”‚ โ”‚ โ”‚ RESPONSE: โ”‚ โ”‚ { โ”‚ โ”‚ "accessToken": "eyJ...", โ”‚ โ”‚ "refreshToken": "eyJ...", โ”‚ โ”‚ "systemUsername": "su_uuid", โ”‚ โ”‚ "username": "johndoe99", โ”‚ โ”‚ "displayName": "John Doe", โ”‚ โ”‚ "currentStep": "COMPLETED", โ”‚ โ”‚ "onboardingComplete": true, โ”‚ โ”‚ "message": "Welcome to NextGate!" โ”‚ โ”‚ } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๐ŸŽ‰ WELCOME! โ”‚ โ”‚ โ”‚ โ”‚ User is fully โ”‚ โ”‚ authenticated and โ”‚ โ”‚ can access the app โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Handling Onboarding Resume When a user returns with incomplete onboarding: const handleOnboardingResponse = (response) => { const { currentStep, nextStep, nextStepMetadata, onboardingToken } = response.data; sessionStorage.setItem('onboardingToken', onboardingToken); switch (nextStep) { case 'AGE_VERIFIED': navigate('/onboarding/age', { state: { metadata: nextStepMetadata } }); break; case 'USERNAME_SET': navigate('/onboarding/username', { state: { metadata: nextStepMetadata } }); break; case 'INTERESTS_SELECTED': navigate('/onboarding/interests'); break; case 'PROFILE_COMPLETED': navigate('/onboarding/profile', { state: { metadata: nextStepMetadata } }); break; case 'COMPLETED': handleFullAuth(response.data); break; } }; Standard Response Format All API responses follow a consistent structure: Success Response Structure : { "success": true, "httpStatus": "OK", "message": "Operation completed successfully", "action_time": "2025-01-24T10:30:45", "data": { } } Error Response Structure : { "success": false, "httpStatus": "BAD_REQUEST", "message": "Error description", "action_time": "2025-01-24T10:30:45", "data": "Error description" } Standard Response Fields : Field Type Description success boolean true for successful operations, false for errors httpStatus string HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) message string Human-readable message action_time string ISO 8601 timestamp data object/string Response payload or error details API Endpoints Authentication Initialization 1. Start Authentication Purpose : Initiate authentication flow for phone, email, or username Endpoint : POST {base_url}/auth/start Access Level : ๐ŸŒ Public Authentication : None Request JSON Sample : { "identifier": "+255712345678", "deviceId": "a1b2c3d4e5f6789", "deviceName": "Chrome on macOS", "platform": "WEB" } Request Body Parameters : Parameter Type Required Description Validation identifier string Yes Phone (+255...), email, or @username Valid format deviceId string Yes Device fingerprint Non-empty deviceName string Yes Human-readable device name Non-empty platform string Yes Device platform enum: IOS , ANDROID , WEB Success Response JSON Sample (New User): { "success": true, "httpStatus": "OK", "message": "Authentication initiated", "action_time": "2025-01-24T10:30:45", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "maskedIdentifier": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78", "provider": "PHONE", "otpExpiresIn": 600, "isNewUser": true, "onboardingComplete": false, "currentStep": "INITIATED", "hasPassword": false, "requiresOtpChannelSelection": false, "message": "OTP sent to โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78" } } Success Response JSON Sample (Existing User with Password): { "success": true, "httpStatus": "OK", "message": "Authentication initiated", "action_time": "2025-01-24T10:30:45", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "maskedIdentifier": "jโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com", "provider": "EMAIL", "otpExpiresIn": 600, "isNewUser": false, "onboardingComplete": true, "currentStep": "COMPLETED", "hasPassword": true, "requiresOtpChannelSelection": false, "message": "Choose login method: password or OTP" } } Success Response JSON Sample (Username - Channel Selection Required): { "success": true, "httpStatus": "OK", "message": "Authentication initiated", "action_time": "2025-01-24T10:30:45", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "hasPassword": true, "requiresOtpChannelSelection": true, "availableChannels": [ { "type": "EMAIL", "maskedValue": "jโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com", "isPrimary": true }, { "type": "PHONE", "maskedValue": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78", "isPrimary": false } ], "isNewUser": false, "onboardingComplete": true, "message": "Select where to receive OTP" } } Success Response Fields : Field Description tempToken Temporary token for next step maskedIdentifier Masked phone/email for display provider Identifier type: PHONE , EMAIL otpExpiresIn OTP validity in seconds (600 = 10 minutes) isNewUser Whether this is a new registration onboardingComplete Whether user completed onboarding currentStep Current onboarding step hasPassword Whether user has set a password requiresOtpChannelSelection If true, user must choose OTP channel availableChannels List of available OTP channels Error Responses : Identifier Blocked (400): { "success": false, "httpStatus": "BAD_REQUEST", "message": "This phone is blocked", "action_time": "2025-01-24T10:30:45", "data": "This phone is blocked" } Account Not Found - Username (404): { "success": false, "httpStatus": "NOT_FOUND", "message": "Account not found", "action_time": "2025-01-24T10:30:45", "data": "Account not found" } 2. Verify Authentication OTP Purpose : Verify OTP and complete authentication or continue to onboarding Endpoint : POST {base_url}/auth/verify Access Level : ๐ŸŒ Public Authentication : None Request JSON Sample : { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "otp": "123456", "deviceId": "a1b2c3d4e5f6789", "deviceName": "Chrome on macOS", "platform": "WEB" } Request Body Parameters : Parameter Type Required Description Validation tempToken string Yes Token from /auth/start Valid JWT otp string Yes 6-digit OTP code Exactly 6 digits deviceId string No Device fingerprint - deviceName string No Human-readable device name - platform string No Device platform enum: IOS , ANDROID , WEB Success Response JSON Sample (New User - Start Onboarding): { "success": true, "httpStatus": "OK", "message": "Verification successful", "action_time": "2025-01-24T10:30:45", "data": { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "onboardingComplete": false, "currentStep": "OTP_VERIFIED", "nextStep": "AGE_VERIFIED", "nextStepMetadata": null, "message": "OTP verified. Let's set up your account." } } Success Response JSON Sample (OAuth User - Has Profile Data): { "success": true, "httpStatus": "OK", "message": "Verification successful", "action_time": "2025-01-24T10:30:45", "data": { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "onboardingComplete": false, "currentStep": "OTP_VERIFIED", "nextStep": "AGE_VERIFIED", "nextStepMetadata": { "firstName": "John", "lastName": "Doe", "profilePicture": "https://lh3.googleusercontent.com/...", "hasOAuthData": true }, "message": "OTP verified. Let's set up your account." } } Success Response JSON Sample (Existing User - Fully Authenticated): { "success": true, "httpStatus": "OK", "message": "Verification successful", "action_time": "2025-01-24T10:30:45", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "systemUsername": "su_550e8400-e29b-41d4-a716-446655440000", "username": "johndoe99", "displayName": "John Doe", "accountTier": "FULL", "onboardingComplete": true, "currentStep": "COMPLETED", "message": "Login successful" } } Error Responses : Invalid OTP (403): { "success": false, "httpStatus": "FORBIDDEN", "message": "Invalid OTP code", "action_time": "2025-01-24T10:30:45", "data": "Invalid OTP code" } Max Attempts Exceeded (403): { "success": false, "httpStatus": "FORBIDDEN", "message": "Maximum verification attempts exceeded", "action_time": "2025-01-24T10:30:45", "data": "Maximum verification attempts exceeded" } 3. Send OTP to Channel Purpose : Send OTP to a specific channel when user has multiple options Endpoint : POST {base_url}/auth/send-otp-to-channel Access Level : ๐ŸŒ Public Authentication : None Request JSON Sample : { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "channel": "EMAIL" } Request Body Parameters : Parameter Type Required Description Validation tempToken string Yes Token from /auth/start Valid JWT channel string Yes Preferred OTP channel enum: EMAIL , PHONE Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "OTP sent", "action_time": "2025-01-24T10:30:45", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "sentTo": "EMAIL", "maskedDestination": "jโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com", "expiresIn": 600, "message": "OTP sent to email" } } 4. Login with Password Purpose : Authenticate using password instead of OTP Endpoint : POST {base_url}/auth/login/password Access Level : ๐ŸŒ Public Authentication : None Request JSON Sample : { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "password": "mySecurePassword123", "deviceId": "a1b2c3d4e5f6789", "deviceName": "Chrome on macOS", "platform": "WEB" } Success Response JSON Sample (Known Device): { "success": true, "httpStatus": "OK", "message": "Login successful", "action_time": "2025-01-24T10:30:45", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "systemUsername": "su_550e8400-e29b-41d4-a716-446655440000", "username": "johndoe99", "displayName": "John Doe", "accountTier": "FULL", "newDevice": false, "requiresDeviceVerification": false, "message": "Login successful" } } Success Response JSON Sample (New Device - Verification Required): { "success": true, "httpStatus": "OK", "message": "Login successful", "action_time": "2025-01-24T10:30:45", "data": { "newDevice": true, "requiresDeviceVerification": true, "deviceVerificationToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "requiresChannelSelection": false, "otpSentTo": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78", "message": "Device verification required. OTP sent." } } Error Responses : Invalid Password (403): { "success": false, "httpStatus": "FORBIDDEN", "message": "Invalid password", "action_time": "2025-01-24T10:30:45", "data": "Invalid password" } 5. OAuth Login (Google) Purpose : Authenticate using Google OAuth2 authorization code flow Endpoint : POST {base_url}/auth/login/oauth Access Level : ๐ŸŒ Public Authentication : None Request JSON Sample : { "provider": "GOOGLE", "code": "4/0AX4XfWh...", "redirectUri": "https://app.nextgate.com/auth/callback", "state": "random-state-string", "deviceId": "a1b2c3d4e5f6789", "deviceName": "Chrome on macOS", "platform": "WEB" } Request Body Parameters : Parameter Type Required Description Validation provider string Yes OAuth provider enum: GOOGLE , APPLE code string Yes Authorization code Non-empty redirectUri string Yes Redirect URI Must match OAuth config state string No State for CSRF protection - deviceId string No Device fingerprint - deviceName string No Device name - platform string No Device platform enum: IOS , ANDROID , WEB Success Response JSON Sample (New User): { "success": true, "httpStatus": "OK", "message": "OAuth login processed", "action_time": "2025-01-24T10:30:45", "data": { "newUser": true, "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "currentStep": "OTP_VERIFIED", "nextStep": "AGE_VERIFIED", "nextStepMetadata": { "firstName": "John", "lastName": "Doe", "profilePicture": "https://lh3.googleusercontent.com/a/...", "hasOAuthData": true }, "message": "Complete your registration", "state": "random-state-string" } } Success Response JSON Sample (Existing User): { "success": true, "httpStatus": "OK", "message": "OAuth login processed", "action_time": "2025-01-24T10:30:45", "data": { "newUser": false, "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "systemUsername": "su_550e8400-e29b-41d4-a716-446655440000", "username": "johndoe99", "displayName": "John Doe", "accountTier": "FULL", "message": "Login successful", "state": "random-state-string" } } 6. Verify Device Purpose : Verify a new device using OTP Endpoint : POST {base_url}/auth/device/verify Access Level : ๐ŸŒ Public Authentication : None Request JSON Sample : { "deviceVerificationToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "otp": "123456", "deviceId": "a1b2c3d4e5f6789", "deviceName": "Chrome on macOS", "platform": "WEB" } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Device verified", "action_time": "2025-01-24T10:30:45", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "systemUsername": "su_550e8400-e29b-41d4-a716-446655440000", "username": "johndoe99", "displayName": "John Doe", "accountTier": "FULL", "message": "Login successful" } } 7. Resend OTP Purpose : Resend OTP code (with rate limiting) Endpoint : POST {base_url}/auth/resend-otp Access Level : ๐ŸŒ Public Authentication : None Request JSON Sample : { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "OTP resend processed", "action_time": "2025-01-24T10:30:45", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "maskedDestination": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78", "sentTo": "PHONE", "expiresIn": 600, "attemptsRemaining": 4, "cooldownSeconds": 0, "message": "OTP resent" } } Cooldown Response : { "success": true, "httpStatus": "OK", "message": "OTP resend processed", "action_time": "2025-01-24T10:30:45", "data": { "cooldownSeconds": 45, "attemptsRemaining": 3, "message": "Please wait 45 seconds before requesting again" } } Onboarding Endpoints 8. Set Age (Step 1) Purpose : Verify user's age and save first/last name Endpoint : POST {base_url}/auth/onboarding/age Access Level : Public (requires valid onboarding token) Request Body : { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "firstName": "John", "lastName": "Doe", "birthDate": "1999-05-15" } Parameter Type Required Description Validation onboardingToken string Yes Token from previous step Valid JWT firstName string Yes User's first name 1-50 characters lastName string Yes User's last name 1-50 characters birthDate string Yes Date of birth ISO date (YYYY-MM-DD), must be in past Success Response : { "success": true, "httpStatus": "OK", "message": "Age verified", "action_time": "2025-01-24T10:30:45", "data": { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "accountTier": "FULL", "age": 25, "restricted": false, "blocked": false, "currentStep": "AGE_VERIFIED", "nextStep": "USERNAME_SET", "nextStepMetadata": { "suggestedUsernames": [ "johndoe99", "john_doe_official", "jdoe_tz", "the_johndoe", "johnd_pro" ] }, "message": "Age verified successfully" } } Blocked Response (Under 13): { "success": true, "httpStatus": "OK", "message": "Age verified", "action_time": "2025-01-24T10:30:45", "data": { "accountTier": null, "age": 12, "restricted": true, "blocked": true, "currentStep": "AGE_VERIFIED", "nextStep": null, "message": "You must be at least 13 years old to use NextGate" } } If age < 13: account is blocked and deleted. If age 13-17: accountTier = RESTRICTED . 9. Check Username Availability Purpose : Real-time username availability check Endpoint : POST {base_url}/auth/onboarding/check-username Access Level : Public Request Body : { "username": "johndoe99" } Response - Available : { "success": true, "httpStatus": "OK", "message": "Username availability checked", "action_time": "2025-01-24T10:30:45", "data": { "username": "johndoe99", "available": true } } Response - Not Available : { "success": true, "httpStatus": "OK", "message": "Username not available", "action_time": "2025-01-24T10:30:45", "data": { "username": "johndoe", "available": false, "suggestions": ["johndoe99", "johndoe_official", "the_johndoe"] } } 10. Set Username (Step 2) Purpose : Set the user's chosen username Endpoint : POST {base_url}/auth/onboarding/username Access Level : Public (requires valid onboarding token) Request Body : { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "username": "johndoe99" } Parameter Type Required Description Validation onboardingToken string Yes Token from previous step Valid JWT username string Yes Chosen username 3-30 chars, starts with letter, alphanumeric + underscore Success Response : { "success": true, "httpStatus": "OK", "message": "Username set", "action_time": "2025-01-24T10:30:45", "data": { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "username": "johndoe99", "available": true, "currentStep": "USERNAME_SET", "nextStep": "CONTACT_VERIFIED", "nextStepMetadata": { "contactVerification": { "requiredContactType": "EMAIL", "requiresInput": true, "alreadyVerified": { "type": "PHONE", "maskedValue": "... ... ..50" }, "reasons": ["TICKET_DELIVERY", "ACCOUNT_RECOVERY", "ACCOUNT_SECURITY"] } }, "message": "Username set successfully" } } requiredContactType depends on signup method: Phone user -> must verify EMAIL Email user -> must verify PHONE Google/Apple user -> must verify PHONE (email already verified via OAuth) 11. Initiate Contact Verification (Step 3) Purpose : Send OTP to the user's secondary contact (email or phone) Endpoint : POST {base_url}/auth/onboarding/contact/initiate Access Level : Public (requires valid onboarding token) Request Body : { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "contactValue": "johndoe@gmail.com" } Parameter Type Required Description Validation onboardingToken string Yes Token from previous step Valid JWT contactValue string Conditional Email or phone to verify Required only if requiresInput = true Success Response : { "success": true, "httpStatus": "OK", "message": "OTP sent", "action_time": "2025-01-24T10:30:45", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "maskedValue": "j......@g.....com", "contactType": "EMAIL", "expiresIn": 600, "verified": false } } 12. Verify Contact OTP (Step 3 - continued) Purpose : Submit the OTP sent to the secondary contact Endpoint : POST {base_url}/auth/onboarding/contact/verify Access Level : Public (requires valid onboarding token) Request Body : { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "otp": "847291" } Parameter Type Required Description onboardingToken string Yes Current onboarding token tempToken string Yes Token returned from initiate endpoint otp string Yes 6-digit OTP code Success Response : { "success": true, "httpStatus": "OK", "message": "Contact verified", "action_time": "2025-01-24T10:30:45", "data": { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "currentStep": "CONTACT_VERIFIED", "nextStep": "INTERESTS_SELECTED", "verified": true } } 13. Edit Contact (Step 3 - inline edit) Purpose : Change the contact value before verifying โ€” no page navigation, inline only Endpoint : POST {base_url}/auth/onboarding/contact/edit Access Level : Public (requires valid onboarding token) Request Body : { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "newContactValue": "newemail@gmail.com" } Parameter Type Required Description onboardingToken string Yes Current onboarding token tempToken string Yes Token from initiate endpoint newContactValue string Yes New email or phone number Success Response : Same shape as initiate โ€” returns a new tempToken and new maskedValue for the updated contact. Previous OTP is invalidated immediately. A fresh OTP is sent to the new contact. Duplicate contact handling : Taken by a verified account -> hard block, error returned Taken by an unverified account -> released automatically, current user proceeds Same account re-entering same contact -> OTP resent, no release cycle 14. Get Interest Categories Purpose : Get all available interest categories for selection Endpoint : GET {base_url}/interests/categories/all Access Level : Public Success Response : { "success": true, "httpStatus": "OK", "message": "Categories retrieved", "action_time": "2025-01-24T10:30:45", "data": [ { "id": "550e8400-e29b-41d4-a716-446655440001", "name": "Music", "icon": "๐ŸŽต", "description": "Concerts, artists, playlists", "displayOrder": 1, "isActive": true }, { "id": "550e8400-e29b-41d4-a716-446655440002", "name": "Sports", "icon": "โšฝ", "description": "Games, teams, fitness", "displayOrder": 2, "isActive": true } ] } 15. Set Interests (Step 4) Purpose : Save user's selected interests Endpoint : POST {base_url}/auth/onboarding/interests Access Level : Public (requires valid onboarding token) Request Body : { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "interestIds": [ "550e8400-e29b-41d4-a716-446655440001", "550e8400-e29b-41d4-a716-446655440002", "550e8400-e29b-41d4-a716-446655440003" ] } Parameter Type Required Description Validation onboardingToken string Yes Token from previous step Valid JWT interestIds array Yes List of category UUIDs Min 3 items Success Response : { "success": true, "httpStatus": "OK", "message": "Interests saved", "action_time": "2025-01-24T10:30:45", "data": { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "selectedInterests": ["Music", "Sports", "Technology"], "count": 3, "currentStep": "INTERESTS_SELECTED", "nextStep": "PROFILE_COMPLETED", "nextStepMetadata": { "firstName": "John", "lastName": "Doe", "username": "johndoe99", "suggestedDisplayName": "John Doe", "profilePicture": "https://lh3.googleusercontent.com/a/..." }, "message": "Interests saved successfully" } } 16. Set Profile (Step 5 - Final) Purpose : Complete profile setup and finish onboarding Endpoint : POST {base_url}/auth/onboarding/profile Access Level : Public (requires valid onboarding token) Request Headers : Header Type Required Description X-Device-Id string No Device fingerprint X-Device-Name string No Device name X-Platform string No IOS , ANDROID , or WEB Request Body : { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "displayName": "John Doe", "bio": "Tech enthusiast | Music lover | Based in Dar es Salaam ๐Ÿ‡น๐Ÿ‡ฟ", "profilePictureUrl": "https://storage.nextgate.com/profiles/abc123.jpg" } Parameter Type Required Description Validation onboardingToken string Yes Token from previous step Valid JWT displayName string Yes Public display name 1-50 characters bio string No User biography Max 160 characters profilePictureUrl string No Profile picture URL Valid URL Success Response : { "success": true, "httpStatus": "OK", "message": "Welcome to NextGate!", "action_time": "2025-01-24T10:30:45", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "systemUsername": "su_550e8400-e29b-41d4-a716-446655440000", "username": "johndoe99", "displayName": "John Doe", "currentStep": "COMPLETED", "onboardingComplete": true, "message": "Welcome to NextGate!" } } 17. Upload Profile Picture (Onboarding Step) Purpose : Upload a profile picture during onboarding, before completing profile setup Endpoint : POST {base_url}/auth/onboarding/upload-profile-picture Access Level : Public (requires valid onboarding token) Request : multipart/form-data Parameter Type Required Description Validation file file Yes Profile picture file Image files only, max 25MB X-Onboarding-Token header Yes Token from previous step Valid onboarding JWT Success Response : { "success": true, "httpStatus": "OK", "message": "Profile picture uploaded", "action_time": "2025-01-24T10:30:45", "data": { "fileName": "a3f1c2d4-...-uuid.jpg", "originalFileName": "my_photo.jpg", "objectKey": "profile/a3f1c2d4-...-uuid.jpg", "directory": "PROFILE", "contentType": "image/jpeg", "fileSize": 204800, "fileSizeFormatted": "200.0 KB", "permanentUrl": "https://files.nextgate.co.tz/bucket-id/profile/a3f1c2d4-...-uuid.jpg", "thumbnailUrl": "https://files.nextgate.co.tz/bucket-id/profile/a3f1c2d4-...-uuid.jpg", "blurHash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH", "fileExtension": ".jpg", "fileType": "IMAGE", "isImage": true, "isVideo": false, "isDocument": false, "isAudio": false, "width": 800, "height": 800, "dimensions": "800x800", "checksum": "d41d8cd98f00b204e9800998ecf8427e", "uploadedAt": "2025-01-24T10:30:45", "isPublic": true } } Use the returned permanentUrl as the value of profilePictureUrl in the subsequent Set Profile request. This endpoint is only accessible when the user has completed the INTERESTS_SELECTED step. Calling it earlier will result in a 403 verification error. Token Management 14. Refresh Access Token Purpose : Get a new access token using refresh token (with rotation) Endpoint : POST {base_url}/auth/token/refresh Access Level : ๐ŸŒ Public Authentication : None (refresh token in body) Request JSON Sample : { "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Token refreshed", "action_time": "2025-01-24T10:30:45", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "tokenType": "Bearer", "expiresIn": 3600, "message": "Tokens refreshed successfully" } } โš ๏ธ Important : The refresh token is rotated on each use. Always store the new refreshToken from the response. Error Response (Token Reuse Detected): { "success": false, "httpStatus": "UNAUTHORIZED", "message": "Security alert: Token reuse detected. Please login again.", "action_time": "2025-01-24T10:30:45", "data": "Security alert: Token reuse detected. Please login again." } 15. Revoke Token Purpose : Revoke a refresh token (logout) Endpoint : POST {base_url}/auth/token/revoke Access Level : ๐ŸŒ Public Authentication : None Request JSON Sample : { "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Token revoked successfully", "action_time": "2025-01-24T10:30:45", "data": null } 16. Refresh Onboarding Token Purpose : Refresh onboarding token if it's about to expire Endpoint : POST {base_url}/auth/token/refresh-onboarding Access Level : ๐ŸŒ Public Authentication : None Request JSON Sample : { "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Onboarding token refreshed", "action_time": "2025-01-24T10:30:45", "data": { "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "currentStep": "USERNAME_SET", "nextStep": "INTERESTS_SELECTED", "expiresIn": 1800, "message": "Token refreshed successfully" } } Password Management 17. Initiate Forgot Password Purpose : Start password reset flow Endpoint : POST {base_url}/auth/password/forgot/initiate Access Level : ๐ŸŒ Public Authentication : None Request JSON Sample : { "identifier": "johndoe99" } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Request processed", "action_time": "2025-01-24T10:30:45", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "maskedDestination": "jโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com", "sentTo": "EMAIL", "expiresIn": 600, "otpVerified": false, "passwordReset": false, "requiresChannelSelection": false, "message": "OTP sent to email" } } 18. Verify Forgot Password OTP Purpose : Verify OTP for password reset Endpoint : POST {base_url}/auth/password/forgot/verify-otp Access Level : ๐ŸŒ Public Authentication : None Request JSON Sample : { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "otp": "123456" } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "OTP verified", "action_time": "2025-01-24T10:30:45", "data": { "resetToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "otpVerified": true, "passwordReset": false, "message": "OTP verified. Set your new password." } } 19. Reset Forgotten Password Purpose : Set new password after OTP verification Endpoint : POST {base_url}/auth/password/forgot/reset Access Level : ๐ŸŒ Public Authentication : None Request JSON Sample : { "resetToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "newPassword": "newSecurePassword123", "confirmPassword": "newSecurePassword123" } Request Body Parameters : Parameter Type Required Description Validation resetToken string Yes Token from verify-otp Valid JWT newPassword string Yes New password Min 8 characters confirmPassword string Yes Confirmation Must match newPassword Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Password reset successfully", "action_time": "2025-01-24T10:30:45", "data": { "otpVerified": true, "passwordReset": true, "message": "Password reset successfully" } } โš ๏ธ Note : After password reset, all active sessions are revoked. User must login again. 20. Change Password (Authenticated) Purpose : Change password for logged-in user Endpoint : POST {base_url}/password/change Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Request Headers : Header Type Required Description Authorization string Yes Bearer {accessToken} Request JSON Sample : { "currentPassword": "oldPassword123", "newPassword": "newSecurePassword456", "confirmPassword": "newSecurePassword456" } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Password changed", "action_time": "2025-01-24T10:30:45", "data": { "success": true, "hadPassword": true, "message": "Password changed successfully" } } 21. Set Password (for Passwordless Users) Purpose : Set password for users who registered via OAuth or OTP only Endpoint : POST {base_url}/password/set Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Request JSON Sample : { "newPassword": "myNewPassword123", "confirmPassword": "myNewPassword123" } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Password set successfully", "action_time": "2025-01-24T10:30:45", "data": { "success": true, "hadPassword": false, "message": "Password set successfully. You can now login with password." } } Session Management 22. Get All Sessions Purpose : List all active sessions for the user Endpoint : GET {base_url}/auth/sessions Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Request Headers : Header Type Required Description Authorization string Yes Bearer {accessToken} X-Session-Id string No Current session ID Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Sessions retrieved", "action_time": "2025-01-24T10:30:45", "data": { "sessions": [ { "id": "550e8400-e29b-41d4-a716-446655440001", "deviceId": "a1b2c3d4e5f6789", "deviceName": "Chrome on macOS", "platform": "WEB", "ipAddress": "192.168.1.1", "location": "Dar es Salaam, Tanzania", "lastActiveAt": "2025-01-24T10:30:45", "createdAt": "2025-01-20T08:00:00", "currentSession": true }, { "id": "550e8400-e29b-41d4-a716-446655440002", "deviceId": "xyz789abc123", "deviceName": "Safari on iPhone", "platform": "IOS", "ipAddress": "192.168.1.2", "location": "Arusha, Tanzania", "lastActiveAt": "2025-01-23T15:20:00", "createdAt": "2025-01-15T12:00:00", "currentSession": false } ], "totalCount": 2 } } 23. Sign Out (Current Session) Purpose : End the current session Endpoint : POST {base_url}/auth/sessions/sign-out Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Request JSON Sample : { "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Signed out successfully", "action_time": "2025-01-24T10:30:45", "data": null } 24. Sign Out All Sessions Purpose : End all sessions including current (security logout) Endpoint : POST {base_url}/auth/sessions/sign-out-all Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "All sessions terminated", "action_time": "2025-01-24T10:30:45", "data": { "terminatedCount": 4, "message": "All 4 sessions have been terminated. Please login again." } } 25. Revoke Specific Session Purpose : End a specific session by ID Endpoint : DELETE {base_url}/auth/sessions/{sessionId} Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Path Parameters : Parameter Type Required Description sessionId string Yes Session UUID to revoke Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Session revoked", "action_time": "2025-01-24T10:30:45", "data": { "sessionId": "550e8400-e29b-41d4-a716-446655440002", "message": "Session has been terminated" } } Device Management 26. Get All Devices Purpose : List all registered/trusted devices Endpoint : GET {base_url}/auth/devices Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Request Headers : Header Type Required Description Authorization string Yes Bearer {accessToken} X-Device-Id string No Current device ID Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Devices retrieved", "action_time": "2025-01-24T10:30:45", "data": { "devices": [ { "id": "550e8400-e29b-41d4-a716-446655440001", "deviceId": "a1b2c3d4e5f6789", "deviceName": "Chrome on macOS", "platform": "WEB", "lastIpAddress": "192.168.1.1", "lastLocation": "Dar es Salaam, Tanzania", "lastActiveAt": "2025-01-24T10:30:45", "firstSeenAt": "2025-01-01T08:00:00", "trustLevel": "TRUSTED", "isCurrentDevice": true } ], "totalCount": 1 } } 27. Remove Device Purpose : Remove a device from trusted devices (revokes all sessions on that device) Endpoint : DELETE {base_url}/auth/devices/{deviceId} Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Path Parameters : Parameter Type Required Description deviceId string Yes Device record UUID Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Device removed", "action_time": "2025-01-24T10:30:45", "data": { "deviceId": "550e8400-e29b-41d4-a716-446655440002", "sessionsRevoked": 2, "message": "Device removed and 2 sessions terminated" } } Account Linking 28. Get Linked Accounts Purpose : List all linked OAuth providers and identifiers Endpoint : GET {base_url}/auth/linked-accounts Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Linked accounts retrieved", "action_time": "2025-01-24T10:30:45", "data": { "linkedAccounts": [ { "type": "EMAIL", "value": "john.doe@gmail.com", "isPrimary": true, "isVerified": true, "linkedAt": "2025-01-01T08:00:00" }, { "type": "PHONE", "value": "+255712345678", "isPrimary": false, "isVerified": true, "linkedAt": "2025-01-05T10:00:00" }, { "type": "OAUTH", "provider": "GOOGLE", "email": "john.doe@gmail.com", "linkedAt": "2025-01-01T08:00:00" } ], "hasPassword": true, "canRemoveEmail": true, "canRemovePhone": true } } 29. Link Email Purpose : Add a new email to the account Endpoint : POST {base_url}/auth/linked-accounts/email/link Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Request JSON Sample : { "email": "john.work@company.com" } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Verification email sent", "action_time": "2025-01-24T10:30:45", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "email": "jโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข@cโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข.com", "expiresIn": 600, "message": "Verification code sent" } } 30. Link Phone Purpose : Add a new phone number to the account Endpoint : POST {base_url}/auth/linked-accounts/phone/link Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Request JSON Sample : { "phone": "+255787654321" } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Verification SMS sent", "action_time": "2025-01-24T10:30:45", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "phone": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข21", "expiresIn": 600, "message": "Verification code sent" } } 31. Link OAuth Provider Purpose : Link a new OAuth provider to existing account Endpoint : POST {base_url}/auth/linked-accounts/oauth/link Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Request JSON Sample : { "provider": "APPLE", "code": "authorization_code_from_apple", "redirectUri": "https://app.nextgate.com/auth/callback" } Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "OAuth provider linked", "action_time": "2025-01-24T10:30:45", "data": { "provider": "APPLE", "email": "john.doe@icloud.com", "linkedAt": "2025-01-24T10:30:45", "message": "Apple account linked successfully" } } 32. Unlink OAuth Provider Purpose : Remove an OAuth provider from the account Endpoint : DELETE {base_url}/auth/linked-accounts/oauth/{provider} Access Level : ๐Ÿ”’ Protected Authentication : Bearer Token Path Parameters : Parameter Type Required Description provider string Yes GOOGLE or APPLE Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "OAuth provider unlinked", "action_time": "2025-01-24T10:30:45", "data": { "provider": "APPLE", "message": "Apple account unlinked" } } Error Response (Last Login Method): { "success": false, "httpStatus": "BAD_REQUEST", "message": "Cannot unlink: This is your only login method.", "action_time": "2025-01-24T10:30:45", "data": "Cannot unlink: This is your only login method." } Error Reference Standard Error Codes HTTP Code Status Common Causes 400 BAD_REQUEST Invalid request data, item already exists 401 UNAUTHORIZED Missing/invalid/expired token 403 FORBIDDEN Invalid OTP, max attempts exceeded, blocked 404 NOT_FOUND Resource doesn't exist 422 UNPROCESSABLE_ENTITY Validation errors 429 TOO_MANY_REQUESTS Rate limit exceeded 500 INTERNAL_SERVER_ERROR Server error Authentication-Specific Errors Error Message HTTP Code Resolution "Token has expired" 401 Refresh token or re-authenticate "Invalid OTP code" 403 Re-enter correct code "Maximum verification attempts exceeded" 403 Request new OTP "This phone is blocked" 400 Contact support "Account not found" 404 Check identifier "Security alert: Token reuse detected" 401 Login again "Login blocked: Too many failed attempts" 400 Wait or reset password "You must be at least 13 years old" N/A Cannot use service Frontend Integration Checklist Before Starting Install FingerprintJS: npm install @fingerprintjs/fingerprintjs Set up secure token storage (httpOnly cookies or secure storage) Configure API base URL Authentication Flow Implement device fingerprint generation on app load Handle all response types from /auth/start Implement OTP input with 6-digit validation Handle device verification flow Store tokens securely after successful auth Onboarding Flow Pre-fill forms using nextStepMetadata when available Display username suggestions as clickable chips Implement real-time username availability check (debounced) Load interest categories from API Require minimum 3 interests Handle profile picture upload Token Management Implement automatic token refresh before expiry Handle 401 responses globally (redirect to login) Store new refresh token after each rotation Clear all tokens on logout Security Best Practices Never store tokens in localStorage - Use httpOnly cookies or secure native storage Always send device fingerprint - Required for device trust tracking Handle token rotation - Always save new refresh token after refresh Validate OTP client-side - Only allow 6 digits before API call Rate limit on frontend - Disable resend button during cooldown Clear tokens on security events - Token reuse detection, password reset End of Documentation New Authentication & Onboarding API (DEPRECATED) Overview Hybrid Auth Strategy: Signup: Phone/Email + OTP (passwordless initially) After onboarding: Optional password setup Login: Password (if set) OR OTP OR Google/Apple Sensitive actions: Require OTP or password confirmation Response Format Standard: All responses follow GlobeSuccessResponseBuilder or GlobeFailureResponseBuilder format: { "success": true/false, "httpStatus": "OK/BAD_REQUEST/etc", "message": "Human readable message", "action_time": "2025-01-11T15:20:00", "data": { ... } } DEVICE SECURITY ARCHITECTURE The Problem We're Solving Without hardware-bound keys: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Attacker steals victim's password โ†“ Attacker generates fake deviceId: "fake-device-123" โ†“ Attacker logs in โ†’ Server asks for OTP โ†“ Attacker does SIM swap / social engineering โ†’ gets OTP โ†“ Attacker is now "trusted" forever ๐Ÿ˜ฑ โ†“ Victim can't kick attacker out (attacker has valid device) With hardware-bound keys: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Attacker steals victim's password โ†“ Attacker tries to login with fake deviceId โ†“ Server: "Sign this challenge with your private key" โ†“ Attacker: "I don't have the private key..." ๐Ÿ˜ค โ†“ Private key is locked inside victim's phone hardware โ†“ Attack FAILS โœ… Asymmetric Cryptography (Key Pair Concept) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ASYMMETRIC CRYPTOGRAPHY โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ Private Key Public Key โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”‚ โ€ข Secret โ€ข Shareable โ”‚ โ”‚ โ€ข Never leaves device โ€ข Stored on server โ”‚ โ”‚ โ€ข Used to SIGN โ€ข Used to VERIFY โ”‚ โ”‚ โ€ข Locked in hardware โ€ข Anyone can have it โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Private Key โ”‚โ”€โ”€โ”€โ”€ generates โ”€โ”€โ–ถโ”‚ Public Key โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ” โ”‚ โ”‚ ๐Ÿ”“ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ Sign("hello") Verify(signature) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ "MEUCIQC7..." true / false โ”‚ โ”‚ (signature) โ”‚ โ”‚ โ”‚ โ”‚ KEY POINT: You CANNOT derive private key from public key โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Platform Security Overview โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ iOS DEVICE โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Main Processor โ”‚ โ”‚ โ”‚ โ”‚ Your app lives here โ”‚ โ”‚ โ”‚ โ”‚ Can request signatures โ”‚ โ”‚ โ”‚ โ”‚ CANNOT access private key โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ "Please sign this" โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ SECURE ENCLAVE (Separate Chip) โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Private Key ๐Ÿ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข Generated HERE โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข Stored HERE โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข NEVER leaves โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข Cannot be read by main processor โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข Cannot be extracted even if jailbroken โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ Returns: signature (NOT the key) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ANDROID DEVICE โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ Option A: StrongBox (Dedicated Security Chip) โ”‚ โ”‚ โ€ข Separate hardware chip (like iOS Secure Enclave) โ”‚ โ”‚ โ€ข Best security โ”‚ โ”‚ โ€ข Available on Pixel 3+, Samsung S10+, etc. โ”‚ โ”‚ โ”‚ โ”‚ Option B: TEE (Trusted Execution Environment) โ”‚ โ”‚ โ€ข Isolated area within main processor โ”‚ โ”‚ โ€ข Very good security โ”‚ โ”‚ โ€ข Available on most Android 7+ devices โ”‚ โ”‚ โ”‚ โ”‚ Both use Android Keystore API โ”‚ โ”‚ Same result: Private key cannot be extracted โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ WEB BROWSER โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ Problem: No dedicated hardware security โ”‚ โ”‚ Solution: PKCE-style ephemeral keys (explained below) โ”‚ โ”‚ โ”‚ โ”‚ โ€ข Generate keypair in MEMORY (not stored) โ”‚ โ”‚ โ€ข Session-bound (dies when tab closes) โ”‚ โ”‚ โ€ข Combined with fingerprint + token binding โ”‚ โ”‚ โ€ข NEVER fully trusted (always require OTP for sensitive) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Platform Security Comparison Aspect iOS Android Web Key Storage Secure Enclave StrongBox / TEE Memory only Hardware Protected โœ… Yes โœ… Yes โŒ No Key Extractable โŒ Never โŒ Never N/A (ephemeral) Forgery Possible โŒ No โŒ No โš ๏ธ Harder Trust Level โญโญโญโญโญ โญโญโญโญโญ โญโญโญ Trust Duration 30 days 30 days 7 days Sensitive Actions Some without OTP Some without OTP ALWAYS OTP Why Attacker Cannot Forge (Mobile) What attacker has access to: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โœ… Victim's email/username (public or leaked) โœ… Victim's password (phishing, data breach, etc.) โœ… Victim's deviceId (could intercept network traffic) โœ… Victim's publicKey (stored on server, not secret) โœ… Can request fresh nonce anytime What attacker CANNOT get: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โŒ Victim's privateKey (locked in hardware) โ€ข Cannot extract from Secure Enclave โ€ข Cannot extract even with physical device access โ€ข Cannot extract even if device is jailbroken Without privateKey: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โŒ Cannot create valid signature โŒ Server rejects login โŒ Attack fails Challenge/Nonce Purpose Without nonce: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 1. Victim logs in legitimately 2. Attacker intercepts: { deviceId, signature } 3. Attacker replays exact same request 4. Server: "Signature valid!" โœ… 5. Attacker is in ๐Ÿ˜ฑ Problem: Signature is always the same for same deviceId With nonce: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 1. Victim logs in legitimately - Gets nonce "ch_abc123" - Signs "ch_abc123|timestamp|deviceId" 2. Attacker intercepts the request 3. Attacker replays exact same request 1 minute later 4. Server: "Nonce ch_abc123 already used/expired!" โŒ 5. Attacker tries to get new nonce and replay - Gets nonce "ch_xyz789" - But old signature was for "ch_abc123" - Signature doesn't match new nonce โŒ 6. Attacker cannot create new signature - Needs privateKey to sign "ch_xyz789|..." - privateKey is in victim's phone โŒ 7. Attack fails โœ… Key insight: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Nonce makes each signature UNIQUE and TIME-LIMITED Even captured valid signatures become useless after ~60 seconds WEB SECURITY MODEL (PKCE-Style) The Problem: Web Cannot Keep Secrets Mobile App: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โœ… Compiled binary (hard to reverse engineer) โœ… Secure storage (Keychain, Keystore) โœ… Hardware protection (Secure Enclave, TEE) โœ… Can store secrets safely Web Browser: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โŒ JavaScript is readable (View Source) โŒ localStorage/IndexedDB accessible via DevTools โŒ No hardware-protected storage โŒ Any "secret" can be extracted If we store a private key in browser: โ†’ Open DevTools โ†’ Application โ†’ IndexedDB โ†’ Copy the key โ†’ Use anywhere ๐Ÿ˜ฑ The Solution: PKCE-Style Ephemeral Keys OAuth2 had the same problem. Their solution: Don't store a secret. Generate a temporary one-time proof. โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ WEB DEVICE AUTH (PKCE-Style) โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ Instead of: โ”‚ โ”‚ Store private key โ†’ Sign challenges โ”‚ โ”‚ (Private key can be stolen from IndexedDB) โ”‚ โ”‚ โ”‚ โ”‚ We do: โ”‚ โ”‚ Each session: Generate fresh keypair in MEMORY โ”‚ โ”‚ Register public key with server for THIS SESSION โ”‚ โ”‚ Private key lives only in JavaScript memory โ”‚ โ”‚ When tab closes โ†’ key is gone โ†’ nothing to steal โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Web Security Layers โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ WEB SECURITY MODEL โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ LAYER 1: Session-Bound Cryptographic Proof โ”‚ โ”‚ โ€ข Generate keypair in memory (not stored) โ”‚ โ”‚ โ€ข Proves: "Same browser tab that started auth" โ”‚ โ”‚ โ€ข Prevents: Request interception/replay โ”‚ โ”‚ โ”‚ โ”‚ LAYER 2: Browser Fingerprint โ”‚ โ”‚ โ€ข Collect browser characteristics โ”‚ โ”‚ โ€ข Proves: "Likely same browser/device" โ”‚ โ”‚ โ€ข Prevents: Token theft to different browser โ”‚ โ”‚ โ”‚ โ”‚ LAYER 3: Bound Tokens โ”‚ โ”‚ โ€ข Tokens bound to fingerprint + IP range โ”‚ โ”‚ โ€ข Server validates on each request โ”‚ โ”‚ โ€ข Prevents: Token theft/export โ”‚ โ”‚ โ”‚ โ”‚ LAYER 4: Never Fully Trust โ”‚ โ”‚ โ€ข Web devices NEVER get "trusted" status โ”‚ โ”‚ โ€ข Sensitive actions ALWAYS require OTP โ”‚ โ”‚ โ€ข Shorter token lifetime than mobile โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Web vs Mobile Trust Levels โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ TRUST LEVEL COMPARISON โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ MOBILE (iOS/Android): โ”‚ โ”‚ After OTP verification: โ”‚ โ”‚ โ€ข Device becomes "TRUSTED" โ”‚ โ”‚ โ€ข Trust lasts 30 days โ”‚ โ”‚ โ€ข Password-only login allowed โ”‚ โ”‚ โ€ข Most actions without re-auth โ”‚ โ”‚ โ€ข Hardware key proves device identity โ”‚ โ”‚ โ”‚ โ”‚ WEB (Browser): โ”‚ โ”‚ After OTP verification: โ”‚ โ”‚ โ€ข Device is "RECOGNIZED" (not trusted) โ”‚ โ”‚ โ€ข Recognition lasts 7 days (shorter) โ”‚ โ”‚ โ€ข Password-only login allowed for basic actions โ”‚ โ”‚ โ€ข Sensitive actions ALWAYS need OTP: โ”‚ โ”‚ โ€ข Change password โ”‚ โ”‚ โ€ข Change email/phone โ”‚ โ”‚ โ€ข View payment methods โ”‚ โ”‚ โ€ข Delete account โ”‚ โ”‚ โ€ข Large purchases โ”‚ โ”‚ โ€ข Session-bound keys (weaker than hardware) โ”‚ โ”‚ โ”‚ โ”‚ WHY THE DIFFERENCE: โ”‚ โ”‚ Mobile: Hardware guarantees "this is THE device" โ”‚ โ”‚ Web: Software only guarantees "probably same browser" โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ AUTHENTICATION FLOWS Device Registration (First App Launch - Mobile) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Device โ”‚ โ”‚ Server โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ 1. Generate key pair in hardware โ”‚ โ”‚ Private key โ†’ Secure Enclave โ”‚ โ”‚ Public key โ†’ exportable โ”‚ โ”‚ โ”‚ โ”‚ 2. GET /auth/challenge โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ โ”‚ โ”‚ โ”‚ 3. { nonce: "ch_abc123", expiresIn: 60 } โ”‚ โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ โ”‚ 4. Sign nonce with private key โ”‚ โ”‚ message = "ch_abc123|timestamp|deviceId"โ”‚ โ”‚ signature = sign(message, privateKey) โ”‚ โ”‚ โ”‚ โ”‚ 5. POST /auth/device/register โ”‚ โ”‚ { โ”‚ โ”‚ deviceId: "ios_xyz...", โ”‚ โ”‚ publicKey: "MFkw...", โ”‚ โ”‚ nonce: "ch_abc123", โ”‚ โ”‚ timestamp: 1736611200000, โ”‚ โ”‚ signature: "MEUC...", โ”‚ โ”‚ platform: "IOS" โ”‚ โ”‚ } โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ โ”‚ โ”‚ โ”‚ 6. Verify signature โ”‚ โ”‚ using publicKey โ”‚ โ”‚ โ”‚ โ”‚ 7. Store: โ”‚ โ”‚ deviceId โ†’ publicKeyโ”‚ โ”‚ โ”‚ โ”‚ 8. { success: true, deviceId: "ios_xyz" } โ”‚ โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ Login Flow (Mobile - With Device Signature) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Device โ”‚ โ”‚ Server โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ 1. GET /auth/challenge โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ โ”‚ โ”‚ โ”‚ 2. { nonce: "ch_xyz789", expiresIn: 60 } โ”‚ โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ โ”‚ 3. Sign: signature = sign( โ”‚ โ”‚ "ch_xyz789|1736611200000|ios_xyz...", โ”‚ โ”‚ privateKey โ”‚ โ”‚ ) โ”‚ โ”‚ โ”‚ โ”‚ 4. POST /auth/login โ”‚ โ”‚ { โ”‚ โ”‚ identifier: "alex@email.com", โ”‚ โ”‚ password: "***", โ”‚ โ”‚ deviceAuth: { โ”‚ โ”‚ deviceId: "ios_xyz...", โ”‚ โ”‚ nonce: "ch_xyz789", โ”‚ โ”‚ timestamp: 1736611200000, โ”‚ โ”‚ signature: "MEUC...", โ”‚ โ”‚ platform: "IOS" โ”‚ โ”‚ } โ”‚ โ”‚ } โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ โ”‚ โ”‚ โ”‚ 5. Validate nonce โ”‚ โ”‚ (exists in Redis?) โ”‚ โ”‚ โ”‚ โ”‚ 6. Delete nonce โ”‚ โ”‚ (one-time use) โ”‚ โ”‚ โ”‚ โ”‚ 7. Get publicKey โ”‚ โ”‚ for deviceId โ”‚ โ”‚ โ”‚ โ”‚ 8. Verify signature โ”‚ โ”‚ โ”‚ โ”‚ 9. Check password โ”‚ โ”‚ โ”‚ โ”‚ 10. Check device trust โ”‚ โ”‚ status โ”‚ โ”‚ โ”‚ โ”‚ 11. { accessToken, refreshToken, ... } โ”‚ โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ Web Session Flow (PKCE-Style) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Browser โ”‚ โ”‚ Server โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ User opens login page โ”‚ โ”‚ โ”‚ โ”‚ 1. Generate keypair IN MEMORY โ”‚ โ”‚ const keyPair = await crypto.subtle โ”‚ โ”‚ .generateKey(ECDSA, P-256) โ”‚ โ”‚ โš ๏ธ NOT stored anywhere โ”‚ โ”‚ โš ๏ธ Lives only in JS variable โ”‚ โ”‚ โ”‚ โ”‚ 2. Generate browser fingerprint โ”‚ โ”‚ โ€ข Screen size, timezone, language โ”‚ โ”‚ โ€ข Canvas hash, WebGL renderer โ”‚ โ”‚ โ€ข โ†’ Hash all into single ID โ”‚ โ”‚ โ”‚ โ”‚ 3. POST /auth/web/session โ”‚ โ”‚ { โ”‚ โ”‚ publicKey: "MFkw...", โ”‚ โ”‚ fingerprint: "fp_abc123..." โ”‚ โ”‚ } โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ โ”‚ โ”‚ โ”‚ 4. Generate sessionId โ”‚ โ”‚ 5. Store in Redis: โ”‚ โ”‚ sessionId โ†’ โ”‚ โ”‚ publicKey, โ”‚ โ”‚ fingerprint, โ”‚ โ”‚ ipAddress โ”‚ โ”‚ TTL: 10 minutes โ”‚ โ”‚ โ”‚ โ”‚ 6. { sessionId: "ws_xyz...", โ”‚ โ”‚ nonce: "ch_abc..." } โ”‚ โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ โ”‚ User enters email + password โ”‚ โ”‚ โ”‚ โ”‚ 7. Sign the nonce โ”‚ โ”‚ signature = sign( โ”‚ โ”‚ nonce + timestamp + sessionId, โ”‚ โ”‚ privateKey โ† still in memory โ”‚ โ”‚ ) โ”‚ โ”‚ โ”‚ โ”‚ 8. POST /auth/login โ”‚ โ”‚ { โ”‚ โ”‚ identifier: "alex@email.com", โ”‚ โ”‚ password: "***", โ”‚ โ”‚ webAuth: { โ”‚ โ”‚ sessionId: "ws_xyz...", โ”‚ โ”‚ nonce: "ch_abc...", โ”‚ โ”‚ timestamp: 1736611200000, โ”‚ โ”‚ signature: "MEUC...", โ”‚ โ”‚ fingerprint: "fp_abc123..." โ”‚ โ”‚ } โ”‚ โ”‚ } โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ โ”‚ โ”‚ โ”‚ 9. Get session from โ”‚ โ”‚ Redis โ”‚ โ”‚ โ”‚ โ”‚ 10. Verify: โ”‚ โ”‚ โ€ข Session exists โ”‚ โ”‚ โ€ข Not expired โ”‚ โ”‚ โ€ข Signature valid โ”‚ โ”‚ โ€ข Fingerprint match โ”‚ โ”‚ โ€ข IP in range โ”‚ โ”‚ โ”‚ โ”‚ 11. Delete session โ”‚ โ”‚ (one-time use) โ”‚ โ”‚ โ”‚ โ”‚ 12. Create BOUND tokens โ”‚ โ”‚ (bound to fp + IP) โ”‚ โ”‚ โ”‚ โ”‚ 13. { โ”‚ โ”‚ accessToken: "...", โ”‚ โ”‚ refreshToken: "...", โ”‚ โ”‚ device: { trusted: false } โ”‚ โ”‚ } โ”‚ โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ SCALABLE ARCHITECTURE โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ NEXTGATE SECURITY ARCHITECTURE โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ API GATEWAY / LOAD BALANCER โ”‚ โ”‚ โ”‚ โ”‚ (Rate Limiting, DDoS Protection) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ AUTH SERVICE (Stateless) โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Challenge/Nonce Service โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Node 1 โ”‚ โ”‚ Node 2 โ”‚ โ”‚ Node 3 โ”‚ โ”‚ Node N โ”‚ (Stateless) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Redis Cluster โ”‚ (Nonce storage, 60s TTL) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (High Availability) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Signature Verification Service โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Node 1 โ”‚ โ”‚ Node 2 โ”‚ โ”‚ Node 3 โ”‚ โ”‚ Node N โ”‚ (Stateless) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ PostgreSQL (Primary-Replica) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ device_keys table (deviceId โ†’ publicKey) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Cached in Redis for fast lookups โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ SCALABILITY: โ”‚ โ”‚ โ€ข Challenge generation: < 5ms (any node can generate) โ”‚ โ”‚ โ€ข Signature verification: < 10ms (pure computation) โ”‚ โ”‚ โ€ข Public keys cached in Redis (1-hour TTL) โ”‚ โ”‚ โ€ข Can handle 10,000+ logins/second per node โ”‚ โ”‚ โ€ข Horizontal scaling: just add more nodes โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ TOKEN BINDING (Web Extra Protection) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ TOKEN BINDING โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ Traditional Token: โ”‚ โ”‚ { โ”‚ โ”‚ "sub": "user_123", โ”‚ โ”‚ "exp": 1736614800 โ”‚ โ”‚ } โ”‚ โ”‚ Problem: Anyone with this token can use it โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Bound Token (What we do for web): โ”‚ โ”‚ { โ”‚ โ”‚ "sub": "user_123", โ”‚ โ”‚ "exp": 1736614800, โ”‚ โ”‚ "device_fp": "fp_abc123...", โ† Must match โ”‚ โ”‚ "ip_hash": "a1b2c3...", โ† Must be in range โ”‚ โ”‚ "platform": "WEB" โ† Affects trust level โ”‚ โ”‚ } โ”‚ โ”‚ โ”‚ โ”‚ On every request, server checks: โ”‚ โ”‚ โ€ข Current fingerprint โ‰ˆ token's device_fp โ”‚ โ”‚ โ€ข Current IP in same /24 range as token's IP โ”‚ โ”‚ โ€ข If mismatch โ†’ reject OR require re-auth โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ BROWSER FINGERPRINT โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ BROWSER FINGERPRINT โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ Components collected: โ”‚ โ”‚ 1. Screen: "1920x1080" โ”‚ โ”‚ 2. Color Depth: 24 โ”‚ โ”‚ 3. Timezone: "Africa/Dar_es_Salaam" โ”‚ โ”‚ 4. Language: "en-US" โ”‚ โ”‚ 5. Platform: "MacIntel" โ”‚ โ”‚ 6. CPU Cores: 8 โ”‚ โ”‚ 7. Memory: 8 (GB, approximate) โ”‚ โ”‚ 8. Canvas Hash: "a1b2c3..." (drawn image fingerprint) โ”‚ โ”‚ 9. WebGL Renderer: "Apple M1" โ”‚ โ”‚ 10. Audio Context fingerprint โ”‚ โ”‚ โ”‚ โ”‚ All combined โ†’ SHA256 โ†’ "fp_7f9a2b3c..." โ”‚ โ”‚ โ”‚ โ”‚ Stability: โ”‚ โ”‚ โ€ข ~90% of users have unique fingerprint โ”‚ โ”‚ โ€ข Changes if: browser update, OS update, new monitor โ”‚ โ”‚ โ€ข We allow ~15% variance (fuzzy matching) โ”‚ โ”‚ โ”‚ โ”‚ Privacy Note: โ”‚ โ”‚ โ€ข NOT used for tracking users โ”‚ โ”‚ โ€ข Only to verify "same browser" for security โ”‚ โ”‚ โ€ข Stored hashed, associated with user session โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ATTACK SCENARIO ANALYSIS ATTACK 1: Steal the private key (Mobile) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Attacker: Tries to extract key from device Result: Key is in Secure Enclave / StrongBox Cannot be extracted even with physical access Verdict: โŒ FAILED ATTACK 2: Steal the private key (Web) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Attacker: Opens DevTools, looks for private key Result: Key is in JavaScript memory, not in storage When attacker opens DevTools, they're in THEIR session with THEIR keypair, not victim's Verdict: โŒ FAILED ATTACK 3: Intercept and replay request โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Attacker: Captures { sessionId, nonce, signature } Attacker: Replays the exact request Server: "Nonce already used/expired" Verdict: โŒ FAILED (nonce is one-time use) ATTACK 4: Get new nonce, use old signature โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Attacker: Gets new nonce "ch_NEW..." Attacker: Uses old signature (signed for "ch_OLD...") Server: Signature doesn't match nonce "ch_NEW..." Verdict: โŒ FAILED (signature bound to specific nonce) ATTACK 5: Steal tokens from victim's browser โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Attacker: Uses XSS to steal accessToken Attacker: Uses token from different browser/IP Server: Fingerprint mismatch! IP range mismatch! Verdict: โŒ FAILED (tokens bound to browser + IP) ATTACK 6: Create fake session from different browser โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Attacker: Knows victim's email + password Attacker: Creates own session (own keypair, own fingerprint) Attacker: Logs in successfully... BUT Server: "New device detected, OTP required" Attacker: Doesn't have victim's phone Verdict: โŒ FAILED (OTP still required for new device) SUMMARY: WHAT WE STORE WHERE โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ WHAT WE STORE WHERE (MOBILE) โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ ON DEVICE (Secure Storage): โ”‚ โ”‚ โœ… Private key (in Secure Enclave / Keystore) โ”‚ โ”‚ โœ… deviceId (identifier) โ”‚ โ”‚ โœ… accessToken (short-lived) โ”‚ โ”‚ โœ… refreshToken (long-lived) โ”‚ โ”‚ โ”‚ โ”‚ ON SERVER: โ”‚ โ”‚ โœ… Public key (for signature verification) โ”‚ โ”‚ โœ… deviceId โ†’ userId mapping โ”‚ โ”‚ โœ… Trust status and expiry โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ WHAT WE STORE WHERE (WEB) โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ IN BROWSER: โ”‚ โ”‚ โœ… accessToken (short-lived, bound) โ”‚ โ”‚ โœ… refreshToken (medium-lived, bound) โ”‚ โ”‚ โœ… deviceId (just an identifier, not secret) โ”‚ โ”‚ โŒ NO private keys stored โ”‚ โ”‚ โŒ NO long-term secrets โ”‚ โ”‚ โ”‚ โ”‚ IN MEMORY ONLY (during session): โ”‚ โ”‚ โœ… Ephemeral keypair (dies when tab closes) โ”‚ โ”‚ โ”‚ โ”‚ ON SERVER: โ”‚ โ”‚ โœ… Session public key (Redis, 10-min TTL) โ”‚ โ”‚ โœ… Browser fingerprint hash โ”‚ โ”‚ โœ… IP range for token binding โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ DEVICE SECURITY API ENDPOINTS 1. Get Challenge (All Platforms) GET /api/v1/auth/challenge Called before any authenticated action (login, register device, refresh token). Response: { "success": true, "httpStatus": "OK", "message": "Challenge generated", "action_time": "2025-01-11T16:00:00Z", "data": { "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U", "expiresIn": 60, "expiresAt": "2025-01-11T16:01:00Z" } } 2. Register Device (Mobile - First Launch) POST /api/v1/auth/device/register Called on first app launch to register the device's public key. Request: { "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...", "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U", "timestamp": 1736611200000, "signature": "MEUCIQC7...", "platform": "IOS" } Field Description deviceId SHA256 hash of public key, prefixed with platform publicKey Base64-encoded ECDSA P-256 public key nonce Challenge from /auth/challenge timestamp Current time in milliseconds signature Sign( nonce|timestamp|deviceId , privateKey) platform IOS , ANDROID , or WEB Response (Success): { "success": true, "httpStatus": "OK", "message": "Device registered successfully", "action_time": "2025-01-11T16:00:05Z", "data": { "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "registered": true, "securityLevel": "SECURE_ENCLAVE", "note": "Device will be linked to user account on login/signup" } } Response (Invalid Signature): { "success": false, "httpStatus": "UNAUTHORIZED", "message": "Invalid device signature", "action_time": "2025-01-11T16:00:05Z", "data": { "code": "INVALID_SIGNATURE", "suggestion": "Ensure keys are generated correctly" } } 3. Initialize Web Session (Web Only) POST /api/v1/auth/web/session Called when user opens login page in browser. Request: { "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...", "fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c" } Field Description publicKey Ephemeral public key (generated in memory) fingerprint Browser fingerprint hash Response: { "success": true, "httpStatus": "OK", "message": "Web session initialized", "action_time": "2025-01-11T16:00:00Z", "data": { "sessionId": "ws_abc123xyz789", "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U", "expiresIn": 600, "expiresAt": "2025-01-11T16:10:00Z" } } 4. Login with Device Auth (Mobile) POST /api/v1/auth/login Request: { "identifier": "alex@example.com", "password": "MySecurePass123!", "deviceAuth": { "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U", "timestamp": 1736611200000, "signature": "MEUCIQC7...", "platform": "IOS" } } Response (Success - Trusted Device): { "success": true, "httpStatus": "OK", "message": "Login successful", "action_time": "2025-01-11T16:00:10Z", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "requiresOtp": false, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "displayName": "Alex Johnson", "profilePictureUrl": "https://storage.example.com/..." }, "device": { "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "trusted": true, "securityLevel": "SECURE_ENCLAVE", "trustExpiresAt": "2025-02-10T16:00:10Z" } } } Response (New Device - OTP Required): { "success": true, "httpStatus": "OK", "message": "New device detected. Please verify with OTP.", "action_time": "2025-01-11T16:00:10Z", "data": { "requiresOtp": true, "otpReason": "NEW_DEVICE", "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpSentTo": "+255*****678", "otpMethod": "SMS", "expiresAt": "2025-01-11T16:10:00Z", "device": { "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "isNew": true, "securityLevel": "SECURE_ENCLAVE" } } } Response (Invalid Signature): { "success": false, "httpStatus": "UNAUTHORIZED", "message": "Invalid device signature", "action_time": "2025-01-11T16:00:10Z", "data": { "code": "INVALID_SIGNATURE", "suggestion": "Please ensure your app is up to date" } } Response (Nonce Expired/Used): { "success": false, "httpStatus": "BAD_REQUEST", "message": "Challenge expired or already used", "action_time": "2025-01-11T16:00:10Z", "data": { "code": "INVALID_NONCE", "suggestion": "Request a new challenge and try again" } } 5. Login with Web Auth (Web Browser) POST /api/v1/auth/login Request: { "identifier": "alex@example.com", "password": "MySecurePass123!", "webAuth": { "sessionId": "ws_abc123xyz789", "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U", "timestamp": 1736611200000, "signature": "MEUCIQC7...", "fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c" } } Response (Success - Web is NEVER fully trusted): { "success": true, "httpStatus": "OK", "message": "Login successful", "action_time": "2025-01-11T16:00:10Z", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "requiresOtp": false, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "displayName": "Alex Johnson" }, "device": { "trusted": false, "recognized": true, "platform": "WEB", "recognitionExpiresAt": "2025-01-18T16:00:10Z", "restrictions": [ "OTP required for password change", "OTP required for email change", "OTP required for phone change", "OTP required for payment actions", "OTP required for account deletion" ] }, "tokenBinding": { "boundToFingerprint": true, "boundToIpRange": true, "note": "Token will be invalidated if used from different browser/network" } } } 6. Verify Device OTP (After New Device Detected) POST /api/v1/auth/device/verify-otp Request: { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpCode": "123456", "trustDevice": true, "deviceAuth": { "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "nonce": "ch_newNonceForVerification", "timestamp": 1736611260000, "signature": "MEUCIQD8...", "platform": "IOS" } } Response (Success): { "success": true, "httpStatus": "OK", "message": "Device verified and trusted", "action_time": "2025-01-11T16:01:00Z", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "displayName": "Alex Johnson" }, "device": { "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "trusted": true, "securityLevel": "SECURE_ENCLAVE", "trustExpiresAt": "2025-02-10T16:01:00Z" } } } 7. Refresh Token (With Device Signature) POST /api/v1/auth/token/refresh Refresh tokens also require device signature to prevent stolen refresh token usage. Request (Mobile): { "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "deviceAuth": { "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "nonce": "ch_refreshNonce123", "timestamp": 1736614800000, "signature": "MEUCIQDx...", "platform": "IOS" } } Request (Web): { "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "webAuth": { "sessionId": "ws_abc123xyz789", "nonce": "ch_refreshNonce123", "timestamp": 1736614800000, "signature": "MEUCIQDx...", "fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c" } } Response: { "success": true, "httpStatus": "OK", "message": "Token refreshed", "action_time": "2025-01-11T17:00:00Z", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600 } } Response (Device Mismatch): { "success": false, "httpStatus": "UNAUTHORIZED", "message": "Token does not belong to this device", "action_time": "2025-01-11T17:00:00Z", "data": { "code": "DEVICE_MISMATCH", "suggestion": "Please login again" } } DEVICE KEYS DATABASE SCHEMA -- Device Keys Table (stores public keys for verification) CREATE TABLE device_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES accounts(id) ON DELETE CASCADE, device_id VARCHAR(64) NOT NULL UNIQUE, -- Cryptographic identity public_key TEXT NOT NULL, key_algorithm VARCHAR(20) DEFAULT 'EC_P256', -- Device metadata (server-derived from User-Agent) platform VARCHAR(20) NOT NULL, -- IOS, ANDROID, WEB device_name VARCHAR(255), security_level VARCHAR(20), -- SECURE_ENCLAVE, STRONGBOX, TEE, SOFTWARE -- Trust status is_trusted BOOLEAN DEFAULT FALSE, trust_expires_at TIMESTAMP, -- Activity tracking last_active_at TIMESTAMP DEFAULT NOW(), last_ip_address VARCHAR(45), last_location VARCHAR(255), -- Audit created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), CONSTRAINT unique_user_device UNIQUE (user_id, device_id) ); CREATE INDEX idx_device_keys_user_id ON device_keys(user_id); CREATE INDEX idx_device_keys_device_id ON device_keys(device_id); CREATE INDEX idx_device_keys_last_active ON device_keys(last_active_at); -- Web Sessions Table (Redis is preferred, but DB fallback) CREATE TABLE web_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id VARCHAR(64) NOT NULL UNIQUE, -- Session data public_key TEXT NOT NULL, fingerprint_hash VARCHAR(64) NOT NULL, ip_address VARCHAR(45), -- Expiry expires_at TIMESTAMP NOT NULL, used_at TIMESTAMP, -- NULL = not used yet -- Audit created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_web_sessions_session_id ON web_sessions(session_id); CREATE INDEX idx_web_sessions_expires ON web_sessions(expires_at); REDIS KEYS STRUCTURE # Nonce/Challenge storage (60 second TTL) nonce:{nonce_value} = {ip_address}|{created_timestamp} # Web session storage (10 minute TTL) websession:{session_id} = { "publicKey": "MFkw...", "fingerprint": "fp_abc123", "ipAddress": "196.41.xxx.xxx", "createdAt": 1736611200000 } # Public key cache (1 hour TTL) pubkey:{device_id} = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..." # Device trust cache (matches trust expiry) devicetrust:{device_id} = { "userId": "550e8400-e29b-41d4-a716-446655440000", "trusted": true, "expiresAt": 1739203200000 } Problem: If username is used in JWT tokens, changing username requires logout (bad UX). Solution: Separate system identifier from display username. Field Type Purpose Can Change? Used In id UUID Primary key โŒ Never DB relations systemUsername String Internal identifier โŒ Never JWT tokens, internal APIs userName String Public @handle โœ… Yes Profile URL, mentions, search, display How it works: systemUsername is auto-generated at signup (e.g., usr_550e8400e29b41d4 ) userName is user-chosen during onboarding (e.g., alexvibes ) JWT tokens contain systemUsername โ†’ user can change userName without logout Profile URLs use userName : app.com/@alexvibes Mentions use userName : @alexvibes Username Change Flow: User changes userName from "alex" to "alexnew" โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 1. Validate new userName โ”‚ โ”‚ 2. Check availability โ”‚ โ”‚ 3. Update userName in DB โ”‚ โ”‚ 4. Return success โ”‚ โ”‚ โ”‚ โ”‚ JWT stays valid (uses โ”‚ โ”‚ systemUsername, unchanged) โ”‚ โ”‚ โ”‚ โ”‚ NO LOGOUT REQUIRED โœ… โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Database: account_table: id UUID PRIMARY KEY system_username VARCHAR(50) UNIQUE NOT NULL -- "usr_550e8400e29b41d4" user_name VARCHAR(30) UNIQUE NOT NULL -- "alexvibes" ... Flow Diagram โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ SIGNUP FLOW (5 Screens) โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ Screen 1: Sign Up Method โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Phone/Email/ โ”‚โ”€โ”€โ–บ OTP Sent โ”€โ”€โ–บ Verify OTP โ”€โ”€โ–บ Account โ”‚ โ”‚ โ”‚ Google/Apple โ”‚ Created โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ Screen 2: Name & Birthdate โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Display Name โ”‚โ”€โ”€โ–บ Validate Age โ”€โ”€โ–บ Save โ”‚ โ”‚ โ”‚ Birthdate โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ Screen 3: Profile Setup โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Profile Pic โ”‚โ”€โ”€โ–บ Username Check โ”€โ”€โ–บ Save โ”‚ โ”‚ โ”‚ Username โ”‚ โ”‚ โ”‚ โ”‚ Bio โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ Screen 4: Interests โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Select 5-10 โ”‚โ”€โ”€โ–บ Save Preferences โ”‚ โ”‚ โ”‚ Categories โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ Screen 5: Complete! โ”€โ”€โ–บ Home Feed โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ LOGIN FLOW โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ DEVICE CHECK FIRST โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Device Info โ”‚โ”€โ”€โ–บ Known & Active? โ”€โ”€โ–บ YES โ”€โ”€โ–บ Continue โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ NO โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ OTP Required First โ”‚ โ”‚ โ”‚ โ”‚ (New/Inactive Device) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ Option A: Password Login (if password set & device trusted) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Identifier + โ”‚โ”€โ”€โ–บ Validate โ”€โ”€โ–บ Access Token โ”‚ โ”‚ โ”‚ Password โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ–บ "Forgot Password?" โ”€โ”€โ–บ Password Reset Flow โ”‚ โ”‚ โ””โ”€โ”€โ–บ "Login with OTP instead" โ”€โ”€โ–บ Option B โ”‚ โ”‚ โ”‚ โ”‚ Option B: Passwordless/OTP Login (always available) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Phone/Email โ”‚โ”€โ”€โ–บ OTP Sent โ”€โ”€โ–บ Verify โ”€โ”€โ–บ Access Token โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ–บ "Lost access to phone/email?" โ”€โ”€โ–บ Account Recovery โ”‚ โ”‚ โ”‚ โ”‚ Option C: Social Login (Google/Apple) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Google/Apple โ”‚โ”€โ”€โ–บ OAuth โ”€โ”€โ–บ Check Existing โ”€โ”€โ–บ Link/Createโ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Email matches existing? โ”‚ โ”‚ โ”‚ โ”‚ YES โ†’ Link Account Flow โ”‚ โ”‚ โ”‚ โ”‚ NO โ†’ Create New โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ Account Recovery (Lost Access) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Verify Identity โ”‚โ”€โ”€โ–บ Support Ticket โ”€โ”€โ–บ Manual Review โ”‚ โ”‚ โ”‚ (ID upload, โ”‚ โ”‚ โ”‚ โ”‚ selfie, etc.) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ SECURITY ARCHITECTURE Why We Need Extra Protection for OAuth Users The Problem: Attacker hacks victim's Google account โ”‚ โ–ผ Attacker can access ALL apps linked to that Google โ”‚ โ–ผ ๐Ÿ˜ฑ If we only rely on Google, attacker owns the account Our Solution: Multi-Layer Security โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ NEXTGATE SECURITY LAYERS โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ Layer 1: Authentication (Who are you?) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Password / OTP / Google / Apple โ”‚ โ”‚ โ”‚ โ”‚ (Any of these can authenticate) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ Layer 2: Device Trust (Is this your device?) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ New device? โ†’ OTP to PHONE required โ”‚ โ”‚ โ”‚ โ”‚ Inactive 30+ days? โ†’ OTP to PHONE required โ”‚ โ”‚ โ”‚ โ”‚ Trusted device? โ†’ Pass through โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ Layer 3: Sensitive Actions (Extra verification) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Change password โ†’ OTP to PHONE โ”‚ โ”‚ โ”‚ โ”‚ Change email โ†’ OTP to PHONE โ”‚ โ”‚ โ”‚ โ”‚ Change phone โ†’ OTP to OLD phone + NEW phone โ”‚ โ”‚ โ”‚ โ”‚ Link/Unlink OAuth โ†’ OTP to PHONE โ”‚ โ”‚ โ”‚ โ”‚ Delete account โ†’ OTP to PHONE + Password (if set) โ”‚ โ”‚ โ”‚ โ”‚ Large purchases โ†’ OTP to PHONE (configurable) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ”‘ KEY INSIGHT: Phone is the ULTIMATE trust anchor โ”‚ โ”‚ Even if Google/Apple/Email is hacked, phone protects you โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Phone as Ultimate Recovery Method Auth Method Compromised Can Attacker Access Account? Password leaked โŒ No - needs device OTP or phone OTP Email hacked โŒ No - sensitive actions need phone OTP Google hacked โŒ No - new device needs phone OTP Apple hacked โŒ No - new device needs phone OTP Phone stolen (unlocked) โš ๏ธ Partial - but needs password for sensitive actions Phone + Password both โœ… Yes - full access (this is expected) Mandatory Phone Verification During onboarding, we STRONGLY encourage phone verification: โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ONBOARDING PHONE PROMPT โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ "Add your phone number for account security" โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ๐Ÿ”’ Recover your account if you lose access โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ”’ Get alerts about suspicious activity โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ”’ Verify sensitive actions โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ [+255] [___________] [Verify] โ”‚ โ”‚ โ”‚ โ”‚ [Skip for now] โ† Show warning: โ”‚ โ”‚ "Without a verified phone, you may lose access to your โ”‚ โ”‚ account if you forget your password or lose access to โ”‚ โ”‚ your Google/Apple account." โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ACCOUNT LINKING & MERGING When OAuth Email Matches Existing Account Scenario Matrix: Existing Account State OAuth Provider Action Email verified + password Google (same email) Prompt to link Email verified + no password Google (same email) Prompt to link Email unverified Google (same email) Auto-link (Google verified it) Phone only (no email set) Google (new email) Prompt to add email or create new Email verified but different Google (different email) Create new account Link Account Flow POST /api/v1/auth/oauth/google Request: { "idToken": "eyJhbGciOiJSUzI1NiIs...", "deviceInfo": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "deviceType": "MOBILE_IOS" } } Response (Email Matches Existing - Link Required): { "success": true, "httpStatus": "OK", "message": "Account found with this email. Please verify to link.", "action_time": "2025-01-11T16:00:00", "data": { "action": "LINK_REQUIRED", "existingAccount": { "id": "550e8400-e29b-41d4-a716-446655440000", "userName": "alexvibes", "displayName": "Alex Johnson", "maskedEmail": "a***@example.com", "maskedPhone": "+255*****678", "hasPassword": true, "authProviders": ["PHONE", "EMAIL"], "createdAt": "2024-06-15T10:00:00" }, "linkOptions": [ { "method": "PASSWORD", "available": true, "description": "Enter your password to link" }, { "method": "OTP_PHONE", "available": true, "description": "Get OTP on +255*****678" }, { "method": "OTP_EMAIL", "available": true, "description": "Get OTP on a***@example.com" } ], "oauthProvider": "GOOGLE", "oauthEmail": "alex@example.com", "tempToken": "eyJhbGciOiJIUzI1NiIs..." } } Confirm Account Link (with Password) POST /api/v1/auth/account/link/confirm Request: { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "method": "PASSWORD", "password": "MySecurePass123!" } Response (Success): { "success": true, "httpStatus": "OK", "message": "Google account linked successfully", "action_time": "2025-01-11T16:01:00", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "displayName": "Alex Johnson", "email": "alex@example.com", "authProviders": ["PHONE", "EMAIL", "GOOGLE"], "onboardingComplete": true }, "linkedProvider": { "provider": "GOOGLE", "email": "alex@example.com", "linkedAt": "2025-01-11T16:01:00" } } } Confirm Account Link (with OTP) POST /api/v1/auth/account/link/request-otp Request: { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "method": "OTP_PHONE" } Response: { "success": true, "httpStatus": "OK", "message": "OTP sent to your phone", "action_time": "2025-01-11T16:01:00", "data": { "otpToken": "eyJhbGciOiJIUzI1NiIs...", "sentTo": "+255*****678", "method": "SMS", "expiresAt": "2025-01-11T16:11:00" } } POST /api/v1/auth/account/link/confirm Request: { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "method": "OTP_PHONE", "otpCode": "123456" } Auto-Link (Unverified Email) When existing account has unverified email that matches Google email: Response (Auto-Linked): { "success": true, "httpStatus": "OK", "message": "Account linked and email verified", "action_time": "2025-01-11T16:00:00", "data": { "action": "AUTO_LINKED", "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "email": "alex@example.com", "isEmailVerified": true, "authProviders": ["PHONE", "GOOGLE"] }, "note": "Your Google account has been linked and email verified automatically" } } Create New Account (No Match) Response (New Account): { "success": true, "httpStatus": "OK", "message": "Account created successfully", "action_time": "2025-01-11T16:00:00", "data": { "action": "CREATED", "isNewUser": true, "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "user": { "id": "660e8400-e29b-41d4-a716-446655440001", "systemUsername": "usr_660e8400e29b41d4", "userName": null, "email": "alex@example.com", "firstName": "Alex", "lastName": "Johnson", "profilePictureUrl": "https://lh3.googleusercontent.com/...", "isEmailVerified": true, "hasPassword": false, "authProviders": ["GOOGLE"], "onboardingStep": "NAME_BIRTHDATE", "onboardingComplete": false }, "promptAddPhone": true, "phonePromptMessage": "Add your phone number to secure your account and enable recovery options" } } MANAGE LINKED ACCOUNTS Get Linked Providers GET /api/v1/auth/providers Response: { "success": true, "httpStatus": "OK", "message": "Linked providers retrieved", "action_time": "2025-01-11T18:00:00", "data": { "providers": [ { "provider": "PHONE", "identifier": "+255*****678", "verified": true, "linkedAt": "2025-01-01T10:00:00", "isPrimary": true, "canUnlink": false, "unlinkBlockedReason": "Phone is your primary recovery method" }, { "provider": "EMAIL", "identifier": "a***@example.com", "verified": true, "linkedAt": "2025-01-01T10:00:00", "isPrimary": false, "canUnlink": true }, { "provider": "GOOGLE", "identifier": "a***@gmail.com", "verified": true, "linkedAt": "2025-01-11T16:01:00", "isPrimary": false, "canUnlink": true } ], "availableToLink": [ { "provider": "APPLE", "description": "Sign in with Apple" } ], "hasPassword": true, "securityNote": "You have 3 ways to access your account" } } Unlink Provider DELETE /api/v1/auth/providers/{provider} Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Request: { "verificationMethod": "OTP_PHONE", "otpCode": "123456" } Response (Success): { "success": true, "httpStatus": "OK", "message": "Google account unlinked", "action_time": "2025-01-11T18:10:00", "data": { "unlinkedProvider": "GOOGLE", "remainingProviders": ["PHONE", "EMAIL"], "securityNote": "You can no longer sign in with Google" } } Response (Cannot Unlink - Only Provider): { "success": false, "httpStatus": "BAD_REQUEST", "message": "Cannot unlink your only sign-in method", "action_time": "2025-01-11T18:10:00", "data": { "code": "CANNOT_UNLINK_ONLY_PROVIDER", "suggestion": "Add another sign-in method before unlinking this one" } } Link New Provider POST /api/v1/auth/providers/link Request: { "provider": "APPLE", "identityToken": "eyJraWQiOiJXNldjT0...", "authorizationCode": "c7e8a3f1b2d4..." } Response: { "success": true, "httpStatus": "OK", "message": "Apple account linked successfully", "action_time": "2025-01-11T18:15:00", "data": { "linkedProvider": { "provider": "APPLE", "identifier": "a***@privaterelay.appleid.com", "linkedAt": "2025-01-11T18:15:00" }, "totalProviders": 4 } } ACCOUNT RECOVERY (Lost Access) When User Can't Access Any Auth Method POST /api/v1/auth/recovery/request Request: { "identifier": "alexvibes", "recoveryReason": "LOST_PHONE", "contactEmail": "backup@anotheremail.com" } Response: { "success": true, "httpStatus": "OK", "message": "Recovery request submitted", "action_time": "2025-01-11T20:00:00", "data": { "ticketId": "REC-2025-001234", "status": "PENDING_REVIEW", "estimatedResponseTime": "24-48 hours", "nextSteps": [ "Check your backup email for instructions", "Prepare identity verification documents", "Our team will contact you within 48 hours" ], "requiredDocuments": [ "Government-issued ID (passport, national ID)", "Selfie holding ID", "Proof of account ownership (screenshots, transaction history)" ] } } SENSITIVE ACTIONS - OTP VERIFICATION All sensitive actions require OTP to phone (regardless of how user logged in): Sensitive Actions List Action OTP Required? Additional Verification Change password โœ… Phone OTP Current password (if set) Change email โœ… Phone OTP - Change phone โœ… Old phone OTP + New phone OTP - Link OAuth provider โœ… Phone OTP - Unlink OAuth provider โœ… Phone OTP - Delete account โœ… Phone OTP Password (if set) View full payment methods โœ… Phone OTP - Add payment method โŒ No - Remove payment method โœ… Phone OTP - Large purchase (>$100) โš™๏ธ Configurable - Export account data โœ… Phone OTP - Change security settings โœ… Phone OTP Password (if set) Request OTP for Sensitive Action POST /api/v1/auth/sensitive-action/request-otp Request: { "action": "CHANGE_EMAIL", "newEmail": "newemail@example.com" } Response: { "success": true, "httpStatus": "OK", "message": "OTP sent to your phone", "action_time": "2025-01-11T19:00:00", "data": { "actionToken": "eyJhbGciOiJIUzI1NiIs...", "action": "CHANGE_EMAIL", "sentTo": "+255*****678", "method": "SMS", "expiresAt": "2025-01-11T19:10:00" } } Confirm Sensitive Action POST /api/v1/auth/sensitive-action/confirm Request: { "actionToken": "eyJhbGciOiJIUzI1NiIs...", "otpCode": "123456" } Response: { "success": true, "httpStatus": "OK", "message": "Email updated successfully", "action_time": "2025-01-11T19:01:00", "data": { "action": "CHANGE_EMAIL", "completed": true, "changes": { "previousEmail": "a***@example.com", "newEmail": "n***@example.com", "emailVerified": false }, "note": "Please verify your new email address" } } NO PHONE? FALLBACK OPTIONS If user didn't add phone during onboarding: Prompt to Add Phone (Shown on sensitive actions) { "success": false, "httpStatus": "FORBIDDEN", "message": "Phone verification required for this action", "action_time": "2025-01-11T19:00:00", "data": { "code": "PHONE_REQUIRED", "action": "CHANGE_EMAIL", "options": [ { "option": "ADD_PHONE", "description": "Add and verify your phone number first", "endpoint": "/api/v1/profile/add-phone" }, { "option": "USE_PASSWORD", "available": true, "description": "Use your password instead (less secure)" }, { "option": "CONTACT_SUPPORT", "description": "Contact support for manual verification" } ] } } SUMMARY: AUTH METHODS & SECURITY Complete Auth Provider Matrix Provider Can Signup? Can Login? Provides Email? Provides Phone? Trust Level Phone + OTP โœ… โœ… โŒ โœ… HIGH Email + OTP โœ… โœ… โœ… โŒ MEDIUM Email + Password โŒ (need OTP first) โœ… โœ… โŒ MEDIUM Google OAuth โœ… โœ… โœ… โŒ MEDIUM Apple OAuth โœ… โœ… โœ… (may be relay) โŒ MEDIUM Password only โŒ โœ… (trusted device) โŒ โŒ LOW Security Recommendations for Users { "securityScore": 75, "level": "GOOD", "recommendations": [ { "priority": "HIGH", "action": "ADD_PHONE", "title": "Verify your phone number", "description": "Enables account recovery and secures sensitive actions", "completed": false }, { "priority": "MEDIUM", "action": "SET_PASSWORD", "title": "Set a password", "description": "Faster login on trusted devices", "completed": true }, { "priority": "LOW", "action": "LINK_BACKUP_EMAIL", "title": "Add backup email", "description": "Alternative recovery option", "completed": false } ] } --- ## SCREEN 1: Sign Up ### 1.1 Initiate Signup (Phone) **POST** `/api/v1/auth/signup/initiate` **Request:** ```json { "method": "PHONE", "phoneNumber": "+255712345678" } Response (Success): { "success": true, "httpStatus": "OK", "message": "OTP sent to your phone", "action_time": "2025-01-11T15:20:00", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "method": "PHONE", "maskedIdentifier": "+255*****678", "expiresAt": "2025-01-11T15:30:00", "resendAllowedAt": "2025-01-11T15:22:00", "attemptsRemaining": 3 } } Response (Phone Already Registered): { "success": false, "httpStatus": "CONFLICT", "message": "This phone number is already registered", "action_time": "2025-01-11T15:20:00", "data": { "code": "ACCOUNT_EXISTS", "field": "phoneNumber", "suggestion": "Please login instead" } } 1.2 Initiate Signup (Email) POST /api/v1/auth/signup/initiate Request: { "method": "EMAIL", "email": "alex@example.com" } Response (Success): { "success": true, "message": "OTP sent to your email", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "method": "EMAIL", "maskedIdentifier": "a***@example.com", "expiresAt": "2025-01-11T15:30:00Z", "resendAllowedAt": "2025-01-11T15:22:00Z", "attemptsRemaining": 3 }, "timestamp": "2025-01-11T15:20:00Z" } 1.3 Verify Signup OTP POST /api/v1/auth/signup/verify Request: { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpCode": "123456" } Response (Success - Account Created): { "success": true, "httpStatus": "OK", "message": "Account created successfully", "action_time": "2025-01-11T15:21:00", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "tokenType": "Bearer", "expiresIn": 3600, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": null, "phoneNumber": "+255712345678", "email": null, "isPhoneVerified": true, "isEmailVerified": false, "hasPassword": false, "onboardingStep": "NAME_BIRTHDATE", "onboardingComplete": false, "createdAt": "2025-01-11T15:20:00" } } } Response (Invalid OTP): { "success": false, "httpStatus": "BAD_REQUEST", "message": "Invalid OTP code", "action_time": "2025-01-11T15:21:00", "data": { "code": "INVALID_OTP", "attemptsRemaining": 2 } } Response (OTP Expired): { "success": false, "httpStatus": "BAD_REQUEST", "message": "OTP has expired", "action_time": "2025-01-11T15:35:00", "data": { "code": "OTP_EXPIRED", "suggestion": "Please request a new OTP" } } 1.4 Resend OTP POST /api/v1/auth/otp/resend Request: { "tempToken": "eyJhbGciOiJIUzI1NiIs..." } Response (Success): { "success": true, "message": "OTP resent successfully", "data": { "newTempToken": "eyJhbGciOiJIUzI1NiIs...", "maskedIdentifier": "+255*****678", "expiresAt": "2025-01-11T15:40:00Z", "resendAllowedAt": "2025-01-11T15:32:00Z", "attemptsRemaining": 2 }, "timestamp": "2025-01-11T15:30:00Z" } Response (Too Soon): { "success": false, "message": "Please wait before requesting another OTP", "error": { "code": "RESEND_COOLDOWN", "resendAllowedAt": "2025-01-11T15:32:00Z", "waitSeconds": 90 }, "timestamp": "2025-01-11T15:30:30Z" } 1.5 Social Signup (Google) POST /api/v1/auth/signup/google Request: { "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..." } Response (Success - New User): { "success": true, "message": "Account created successfully", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "isNewUser": true, "user": { "id": "550e8400-e29b-41d4-a716-446655440001", "email": "alex@gmail.com", "firstName": "Alex", "lastName": "Johnson", "profilePictureUrl": "https://lh3.googleusercontent.com/...", "isEmailVerified": true, "hasPassword": false, "authProvider": "GOOGLE", "onboardingStep": "NAME_BIRTHDATE", "onboardingComplete": false, "createdAt": "2025-01-11T15:20:00Z" } }, "timestamp": "2025-01-11T15:20:00Z" } Response (Existing User - Login): { "success": true, "message": "Welcome back!", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "isNewUser": false, "user": { "id": "550e8400-e29b-41d4-a716-446655440001", "userName": "alexj", "email": "alex@gmail.com", "displayName": "Alex Johnson", "profilePictureUrl": "https://storage.example.com/...", "isEmailVerified": true, "hasPassword": true, "authProvider": "GOOGLE", "onboardingComplete": true } }, "timestamp": "2025-01-11T15:20:00Z" } 1.6 Social Signup (Apple) POST /api/v1/auth/signup/apple Request: { "identityToken": "eyJraWQiOiJXNldjT0...", "authorizationCode": "c7e8a3f1b2d4...", "firstName": "Alex", "lastName": "Johnson" } Note: Apple only provides name on first authorization Response: Same structure as Google response SCREEN 2: Name & Birthdate 2.1 Save Name & Birthdate PUT /api/v1/onboarding/name-birthdate Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Request: { "displayName": "Alex Johnson", "firstName": "Alex", "lastName": "Johnson", "birthDate": "1995-06-15" } Response (Success): { "success": true, "message": "Profile updated successfully", "data": { "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "displayName": "Alex Johnson", "firstName": "Alex", "lastName": "Johnson", "birthDate": "1995-06-15", "age": 29, "onboardingStep": "PROFILE_SETUP", "onboardingComplete": false } }, "timestamp": "2025-01-11T15:22:00Z" } Response (Underage - Below 13): { "success": false, "message": "You must be at least 13 years old to use this app", "error": { "code": "UNDERAGE", "field": "birthDate", "minimumAge": 13 }, "timestamp": "2025-01-11T15:22:00Z" } Response (Invalid Date): { "success": false, "message": "Invalid birth date", "error": { "code": "INVALID_DATE", "field": "birthDate", "details": "Birth date cannot be in the future" }, "timestamp": "2025-01-11T15:22:00Z" } SCREEN 3: Profile Setup 3.1 Check Username Availability GET /api/v1/onboarding/username/check?username=alexvibes Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Note: Always returns suggestions (even when available) so user can pick alternatives Response (Available): { "success": true, "httpStatus": "OK", "message": "Username is available", "action_time": "2025-01-11T15:23:00", "data": { "username": "alexvibes", "available": true, "valid": true, "suggestions": [ "alexvibes_", "alexvibes1", "thealexvibes", "alexvibes_official", "real_alexvibes" ] } } Response (Taken): { "success": true, "httpStatus": "OK", "message": "Username is already taken", "action_time": "2025-01-11T15:23:00", "data": { "username": "alex", "available": false, "valid": true, "suggestions": [ "alex123", "alex_vibes", "alexcool", "alex2025", "thealex" ] } } Response (Invalid Format): { "success": true, "httpStatus": "OK", "message": "Invalid username format", "action_time": "2025-01-11T15:23:00", "data": { "username": "123alex", "available": false, "valid": false, "validationError": "Username must start with a letter", "suggestions": [ "alex123", "alex_user", "alexnew", "user_alex", "the_alex" ] } } 3.2 Upload Profile Picture POST /api/v1/onboarding/profile-picture Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Content-Type: multipart/form-data Request (Form Data): file: [binary image data] Response (Success): { "success": true, "message": "Profile picture uploaded successfully", "data": { "profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380.jpg", "thumbnailUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380_thumb.jpg" }, "timestamp": "2025-01-11T15:23:00Z" } Response (Invalid File): { "success": false, "message": "Invalid file type", "error": { "code": "INVALID_FILE_TYPE", "allowedTypes": ["image/jpeg", "image/png", "image/webp"], "maxSizeMB": 5 }, "timestamp": "2025-01-11T15:23:00Z" } 3.3 Save Profile Setup PUT /api/v1/onboarding/profile-setup Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Request: { "userName": "alexvibes", "bio": "Fashion lover | Shopaholic | Event creator ๐Ÿ”ฅ", "profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380.jpg" } Response (Success): { "success": true, "message": "Profile setup completed", "data": { "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "userName": "alexvibes", "displayName": "Alex Johnson", "bio": "Fashion lover | Shopaholic | Event creator ๐Ÿ”ฅ", "profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380.jpg", "onboardingStep": "INTERESTS", "onboardingComplete": false } }, "timestamp": "2025-01-11T15:24:00Z" } Response (Username Taken - Race Condition): { "success": false, "message": "Username was just taken by another user", "error": { "code": "USERNAME_TAKEN", "field": "userName", "suggestions": [ "alexvibes1", "alexvibes_", "thealexvibes" ] }, "timestamp": "2025-01-11T15:24:00Z" } SCREEN 4: Interests 4.1 Get Available Interest Categories GET /api/v1/onboarding/interests/categories Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Response: { "success": true, "message": "Categories retrieved successfully", "data": { "categories": [ { "id": "cat_001", "name": "Fashion", "icon": "๐Ÿ‘—", "iconUrl": "https://cdn.example.com/icons/fashion.png", "color": "#FF6B6B" }, { "id": "cat_002", "name": "Electronics", "icon": "๐Ÿ“ฑ", "iconUrl": "https://cdn.example.com/icons/electronics.png", "color": "#4ECDC4" }, { "id": "cat_003", "name": "Beauty & Cosmetics", "icon": "๐Ÿ’„", "iconUrl": "https://cdn.example.com/icons/beauty.png", "color": "#FF69B4" }, { "id": "cat_004", "name": "Food & Drinks", "icon": "๐Ÿ”", "iconUrl": "https://cdn.example.com/icons/food.png", "color": "#F39C12" }, { "id": "cat_005", "name": "Sports & Fitness", "icon": "โšฝ", "iconUrl": "https://cdn.example.com/icons/sports.png", "color": "#2ECC71" }, { "id": "cat_006", "name": "Music & Dance", "icon": "๐ŸŽต", "iconUrl": "https://cdn.example.com/icons/music.png", "color": "#9B59B6" }, { "id": "cat_007", "name": "Home & Decor", "icon": "๐Ÿ ", "iconUrl": "https://cdn.example.com/icons/home.png", "color": "#E67E22" }, { "id": "cat_008", "name": "Tech & Gadgets", "icon": "๐Ÿ’ป", "iconUrl": "https://cdn.example.com/icons/tech.png", "color": "#3498DB" }, { "id": "cat_009", "name": "Travel", "icon": "โœˆ๏ธ", "iconUrl": "https://cdn.example.com/icons/travel.png", "color": "#1ABC9C" }, { "id": "cat_010", "name": "Gaming", "icon": "๐ŸŽฎ", "iconUrl": "https://cdn.example.com/icons/gaming.png", "color": "#8E44AD" }, { "id": "cat_011", "name": "Books & Reading", "icon": "๐Ÿ“š", "iconUrl": "https://cdn.example.com/icons/books.png", "color": "#D35400" }, { "id": "cat_012", "name": "Art & Design", "icon": "๐ŸŽจ", "iconUrl": "https://cdn.example.com/icons/art.png", "color": "#E74C3C" }, { "id": "cat_013", "name": "Health & Wellness", "icon": "๐Ÿง˜", "iconUrl": "https://cdn.example.com/icons/wellness.png", "color": "#27AE60" }, { "id": "cat_014", "name": "Automotive", "icon": "๐Ÿš—", "iconUrl": "https://cdn.example.com/icons/auto.png", "color": "#34495E" }, { "id": "cat_015", "name": "Pets & Animals", "icon": "๐Ÿพ", "iconUrl": "https://cdn.example.com/icons/pets.png", "color": "#F1C40F" }, { "id": "cat_016", "name": "Photography", "icon": "๐Ÿ“ท", "iconUrl": "https://cdn.example.com/icons/photo.png", "color": "#7F8C8D" }, { "id": "cat_017", "name": "Kids & Baby", "icon": "๐Ÿ‘ถ", "iconUrl": "https://cdn.example.com/icons/kids.png", "color": "#FFB6C1" }, { "id": "cat_018", "name": "Business & Finance", "icon": "๐Ÿ’ผ", "iconUrl": "https://cdn.example.com/icons/business.png", "color": "#2C3E50" }, { "id": "cat_019", "name": "Entertainment", "icon": "๐ŸŽฌ", "iconUrl": "https://cdn.example.com/icons/entertainment.png", "color": "#C0392B" }, { "id": "cat_020", "name": "DIY & Crafts", "icon": "๐Ÿ› ๏ธ", "iconUrl": "https://cdn.example.com/icons/diy.png", "color": "#16A085" } ], "minimumSelection": 3, "recommendedSelection": 5, "maximumSelection": 15 }, "timestamp": "2025-01-11T15:25:00Z" } 4.2 Save User Interests POST /api/v1/onboarding/interests Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Request: { "categoryIds": [ "cat_001", "cat_003", "cat_006", "cat_009", "cat_012" ] } Response (Success): { "success": true, "message": "Interests saved successfully", "data": { "selectedCount": 5, "interests": [ { "id": "cat_001", "name": "Fashion" }, { "id": "cat_003", "name": "Beauty & Cosmetics" }, { "id": "cat_006", "name": "Music & Dance" }, { "id": "cat_009", "name": "Travel" }, { "id": "cat_012", "name": "Art & Design" } ], "onboardingStep": "COMPLETE", "onboardingComplete": true }, "timestamp": "2025-01-11T15:26:00Z" } Response (Too Few Selected): { "success": false, "message": "Please select at least 3 interests", "error": { "code": "MINIMUM_NOT_MET", "selectedCount": 1, "minimumRequired": 3 }, "timestamp": "2025-01-11T15:26:00Z" } 4.3 Skip Interests (Optional) POST /api/v1/onboarding/interests/skip Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Request: {} Response: { "success": true, "message": "Interests skipped. You can update them later in settings.", "data": { "onboardingStep": "COMPLETE", "onboardingComplete": true, "reminder": "We'll show you general content. Update your interests anytime for a personalized feed!" }, "timestamp": "2025-01-11T15:26:00Z" } SCREEN 5: Complete Onboarding 5.1 Get Onboarding Summary & Suggestions GET /api/v1/onboarding/complete Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Response: { "success": true, "message": "Welcome to the app! ๐ŸŽ‰", "data": { "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "userName": "alexvibes", "displayName": "Alex Johnson", "profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic.jpg", "bio": "Fashion lover | Shopaholic | Event creator ๐Ÿ”ฅ", "onboardingComplete": true, "hasPassword": false }, "suggestions": { "accountsToFollow": [ { "id": "user_001", "userName": "fashionista", "displayName": "Fashion Hub", "profilePictureUrl": "https://storage.example.com/...", "bio": "Latest fashion trends", "isVerified": true, "followerCount": 12500, "matchReason": "Based on your interest in Fashion" }, { "id": "user_002", "userName": "beautyguru", "displayName": "Beauty Tips Daily", "profilePictureUrl": "https://storage.example.com/...", "bio": "Makeup tutorials & reviews", "isVerified": true, "followerCount": 8300, "matchReason": "Based on your interest in Beauty & Cosmetics" } ], "shopsToFollow": [ { "id": "shop_001", "name": "Style Studio", "logoUrl": "https://storage.example.com/...", "category": "Fashion", "rating": 4.8, "productCount": 156, "matchReason": "Top rated in Fashion" } ], "upcomingEvents": [ { "id": "event_001", "title": "Summer Fashion Show", "coverImageUrl": "https://storage.example.com/...", "startDate": "2025-01-20T18:00:00Z", "attendeeCount": 234, "matchReason": "Fashion event near you" } ] }, "nextSteps": [ { "action": "FOLLOW_ACCOUNTS", "title": "Follow 5 accounts", "description": "Get started by following accounts you like", "reward": null }, { "action": "SET_PASSWORD", "title": "Set a password", "description": "For faster login next time", "reward": null }, { "action": "FIRST_POST", "title": "Create your first post", "description": "Share something with the community", "reward": null } ] }, "timestamp": "2025-01-11T15:27:00Z" } LOGIN FLOWS (With Device Trust) Login Option A: Password Login POST /api/v1/auth/login/password Request: { "identifier": "alexvibes", "password": "MySecurePass123!", "deviceInfo": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "deviceType": "MOBILE_IOS", "appVersion": "1.2.0" } } identifier can be: username, email, or phone number Response (Success - Trusted Device): { "success": true, "httpStatus": "OK", "message": "Login successful", "action_time": "2025-01-11T16:00:00", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "requiresOtp": false, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "displayName": "Alex Johnson", "profilePictureUrl": "https://storage.example.com/...", "email": "alex@example.com", "phoneNumber": "+255712345678", "isEmailVerified": true, "isPhoneVerified": true, "hasPassword": true, "onboardingComplete": true }, "device": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "trusted": true, "trustExpiresAt": "2025-02-10T16:00:00" } } } Response (New Device - OTP Required): { "success": true, "httpStatus": "OK", "message": "New device detected. Please verify with OTP.", "action_time": "2025-01-11T16:00:00", "data": { "requiresOtp": true, "otpReason": "NEW_DEVICE", "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpSentTo": "+255*****678", "otpMethod": "SMS", "expiresAt": "2025-01-11T16:10:00", "device": { "deviceId": "xyz789-new-device", "deviceName": "Chrome on Windows", "isNew": true }, "securityNote": "We detected a login from a new device. For your security, please verify with OTP." } } Response (Inactive Device 30+ days - OTP Required): { "success": true, "httpStatus": "OK", "message": "Device inactive for 30+ days. Please verify with OTP.", "action_time": "2025-01-11T16:00:00", "data": { "requiresOtp": true, "otpReason": "INACTIVE_DEVICE", "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpSentTo": "+255*****678", "otpMethod": "SMS", "expiresAt": "2025-01-11T16:10:00", "device": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "lastActiveAt": "2024-12-01T10:00:00", "inactiveDays": 41 }, "securityNote": "This device hasn't been used in 41 days. Please verify it's you." } } Response (Wrong Password): { "success": false, "httpStatus": "UNAUTHORIZED", "message": "Invalid credentials", "action_time": "2025-01-11T16:00:00", "data": { "code": "INVALID_CREDENTIALS", "attemptsRemaining": 4, "suggestion": "Forgot password? Use OTP login instead" } } Response (Account Locked): { "success": false, "httpStatus": "LOCKED", "message": "Account temporarily locked due to multiple failed attempts", "action_time": "2025-01-11T16:00:00", "data": { "code": "ACCOUNT_LOCKED", "unlockAt": "2025-01-11T16:30:00", "suggestion": "Try OTP login or wait 30 minutes" } } Response (No Password Set): { "success": false, "httpStatus": "BAD_REQUEST", "message": "No password set for this account", "action_time": "2025-01-11T16:00:00", "data": { "code": "NO_PASSWORD", "suggestion": "Use OTP login or social login" } } Login Option B: OTP Login (Passwordless) B.1 Request OTP POST /api/v1/auth/login/otp/request Request: { "identifier": "+255712345678", "deviceInfo": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "deviceType": "MOBILE_IOS", "appVersion": "1.2.0" } } identifier can be: email or phone number Response (Success): { "success": true, "httpStatus": "OK", "message": "OTP sent to your phone", "action_time": "2025-01-11T16:00:00", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "method": "SMS", "maskedIdentifier": "+255*****678", "expiresAt": "2025-01-11T16:10:00", "resendAllowedAt": "2025-01-11T16:02:00", "device": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "isNew": false, "lastActiveAt": "2025-01-10T14:30:00" } } } Response (User Not Found): { "success": false, "httpStatus": "NOT_FOUND", "message": "No account found with this phone number", "action_time": "2025-01-11T16:00:00", "data": { "code": "USER_NOT_FOUND", "suggestion": "Would you like to create an account?", "createAccountUrl": "/api/v1/auth/signup/initiate" } } B.2 Verify OTP Login POST /api/v1/auth/login/otp/verify Request: { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpCode": "123456", "trustDevice": true, "deviceInfo": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "deviceType": "MOBILE_IOS", "appVersion": "1.2.0" } } Response (Success - Device Trusted): { "success": true, "httpStatus": "OK", "message": "Login successful", "action_time": "2025-01-11T16:01:00", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "displayName": "Alex Johnson", "profilePictureUrl": "https://storage.example.com/...", "hasPassword": false, "onboardingComplete": true }, "device": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "trusted": true, "trustExpiresAt": "2025-02-10T16:01:00" }, "promptSetPassword": true, "passwordPromptMessage": "Set a password for faster login on trusted devices" } } Response (Success - Device NOT Trusted by user choice): { "success": true, "httpStatus": "OK", "message": "Login successful (device not saved)", "action_time": "2025-01-11T16:01:00", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "displayName": "Alex Johnson", "profilePictureUrl": "https://storage.example.com/...", "hasPassword": false, "onboardingComplete": true }, "device": { "trusted": false, "note": "This device will require OTP on next login" } } } Login Option C: Social Login (Google/Apple) POST /api/v1/auth/oauth/google Request: { "idToken": "eyJhbGciOiJSUzI1NiIs...", "deviceInfo": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "deviceType": "MOBILE_IOS", "appVersion": "1.2.0" } } Response (Existing User - Trusted Device): { "success": true, "httpStatus": "OK", "message": "Welcome back!", "action_time": "2025-01-11T16:00:00", "data": { "action": "LOGIN", "isNewUser": false, "requiresOtp": false, "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "displayName": "Alex Johnson", "email": "alex@gmail.com", "profilePictureUrl": "https://storage.example.com/...", "isEmailVerified": true, "hasPassword": true, "authProviders": ["PHONE", "EMAIL", "GOOGLE"], "onboardingComplete": true }, "device": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "trusted": true, "trustExpiresAt": "2025-02-10T16:00:00" } } } Response (Existing User - New Device - OTP Required): { "success": true, "httpStatus": "OK", "message": "New device detected. Please verify with OTP.", "action_time": "2025-01-11T16:00:00", "data": { "action": "DEVICE_VERIFICATION_REQUIRED", "isNewUser": false, "requiresOtp": true, "otpReason": "NEW_DEVICE", "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpSentTo": "+255*****678", "otpMethod": "SMS", "expiresAt": "2025-01-11T16:10:00", "user": { "userName": "alexvibes", "displayName": "Alex Johnson", "profilePictureUrl": "https://storage.example.com/..." }, "device": { "deviceId": "xyz789-new-device", "deviceName": "Chrome on Windows", "isNew": true }, "securityNote": "Even though you signed in with Google, we need to verify this new device." } } Response (Existing User - No Phone - Fallback Options): { "success": true, "httpStatus": "OK", "message": "New device detected. Please verify to continue.", "action_time": "2025-01-11T16:00:00", "data": { "action": "VERIFICATION_REQUIRED_NO_PHONE", "isNewUser": false, "requiresVerification": true, "tempToken": "eyJhbGciOiJIUzI1NiIs...", "user": { "userName": "alexvibes", "displayName": "Alex Johnson" }, "device": { "deviceId": "xyz789-new-device", "deviceName": "Chrome on Windows", "isNew": true }, "verificationOptions": [ { "method": "ADD_PHONE", "description": "Add phone number to receive OTP", "recommended": true }, { "method": "OTP_EMAIL", "description": "Send OTP to a***@gmail.com", "available": true, "lessSecure": true, "note": "Email is less secure than phone" }, { "method": "PASSWORD", "available": true, "description": "Enter your password" } ], "securityNote": "For better security, we recommend adding a phone number." } } Response (New User - Account Created): { "success": true, "httpStatus": "OK", "message": "Account created successfully", "action_time": "2025-01-11T16:00:00", "data": { "action": "CREATED", "isNewUser": true, "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "user": { "id": "660e8400-e29b-41d4-a716-446655440001", "systemUsername": "usr_660e8400e29b41d4", "userName": null, "email": "alex@gmail.com", "firstName": "Alex", "lastName": "Johnson", "profilePictureUrl": "https://lh3.googleusercontent.com/...", "isEmailVerified": true, "hasPassword": false, "authProviders": ["GOOGLE"], "onboardingStep": "NAME_BIRTHDATE", "onboardingComplete": false }, "device": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "trusted": true, "trustExpiresAt": "2025-02-10T16:00:00", "note": "First device is automatically trusted" }, "promptAddPhone": true, "phonePromptMessage": "Add your phone number to secure your account" } } Response (Email Matches Existing - Link Required): { "success": true, "httpStatus": "OK", "message": "Account found with this email. Please verify to link.", "action_time": "2025-01-11T16:00:00", "data": { "action": "LINK_REQUIRED", "isNewUser": false, "existingAccount": { "id": "550e8400-e29b-41d4-a716-446655440000", "userName": "alexvibes", "displayName": "Alex Johnson", "maskedEmail": "a***@example.com", "maskedPhone": "+255*****678", "hasPassword": true, "authProviders": ["PHONE", "EMAIL"], "createdAt": "2024-06-15T10:00:00" }, "linkOptions": [ { "method": "PASSWORD", "available": true, "description": "Enter your password to link" }, { "method": "OTP_PHONE", "available": true, "description": "Get OTP on +255*****678" }, { "method": "OTP_EMAIL", "available": true, "description": "Get OTP on a***@example.com" } ], "oauthProvider": "GOOGLE", "oauthEmail": "alex@gmail.com", "tempToken": "eyJhbGciOiJIUzI1NiIs...", "device": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "note": "Device will be trusted after account link" } } } Apple Sign In POST /api/v1/auth/oauth/apple Request: { "identityToken": "eyJraWQiOiJXNldjT0...", "authorizationCode": "c7e8a3f1b2d4...", "firstName": "Alex", "lastName": "Johnson", "deviceInfo": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "deviceType": "MOBILE_IOS", "appVersion": "1.2.0" } } Note: Apple only provides name on first authorization. Store it! Responses: Same structure as Google OAuth responses above. Verify Device OTP (After any login triggers device verification) POST /api/v1/auth/login/verify-device Request: { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpCode": "123456", "trustDevice": true, "deviceInfo": { "deviceId": "xyz789-new-device", "deviceName": "Chrome on Windows", "deviceType": "WEB_BROWSER" } } Response (Success): { "success": true, "httpStatus": "OK", "message": "Device verified and trusted", "action_time": "2025-01-11T16:02:00", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "displayName": "Alex Johnson", "profilePictureUrl": "https://storage.example.com/...", "onboardingComplete": true }, "device": { "deviceId": "xyz789-new-device", "deviceName": "Chrome on Windows", "trusted": true, "trustExpiresAt": "2025-02-10T16:02:00" } } } POST-ONBOARDING: Set Password (Optional) POST /api/v1/auth/password/set Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Request: { "newPassword": "MySecurePass123!", "confirmPassword": "MySecurePass123!" } Response (Success): { "success": true, "message": "Password set successfully. You can now use password login.", "data": { "hasPassword": true, "passwordStrength": { "score": 85, "level": "STRONG", "feedback": "Great password!" } }, "timestamp": "2025-01-11T16:05:00Z" } Response (Weak Password): { "success": false, "message": "Password is too weak", "error": { "code": "WEAK_PASSWORD", "passwordStrength": { "score": 40, "level": "WEAK", "feedback": "Add uppercase letters and special characters" }, "requirements": [ "At least 8 characters", "At least one uppercase letter", "At least one lowercase letter", "At least one number", "At least one special character (@$!%*?&#)" ] }, "timestamp": "2025-01-11T16:05:00Z" } TOKEN MANAGEMENT Refresh Token POST /api/v1/auth/token/refresh Request: { "refreshToken": "eyJhbGciOiJIUzI1NiIs..." } Response (Success): { "success": true, "message": "Token refreshed successfully", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600 }, "timestamp": "2025-01-11T17:00:00Z" } Response (Invalid/Expired Refresh Token): { "success": false, "message": "Session expired. Please login again.", "error": { "code": "REFRESH_TOKEN_EXPIRED" }, "timestamp": "2025-01-11T17:00:00Z" } Logout POST /api/v1/auth/logout Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Request: { "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "logoutAllDevices": false } Response: { "success": true, "message": "Logged out successfully", "data": null, "timestamp": "2025-01-11T18:00:00Z" } PASSWORD RESET Request Password Reset POST /api/v1/auth/password/reset/request Request: { "identifier": "alex@example.com" } Response: { "success": true, "message": "Password reset OTP sent to your email", "data": { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "maskedIdentifier": "a***@example.com", "expiresAt": "2025-01-11T16:10:00Z" }, "timestamp": "2025-01-11T16:00:00Z" } Verify Reset OTP & Set New Password POST /api/v1/auth/password/reset/confirm Request: { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpCode": "123456", "newPassword": "MyNewSecurePass123!", "confirmPassword": "MyNewSecurePass123!" } Response: { "success": true, "message": "Password reset successfully. Please login with your new password.", "data": null, "timestamp": "2025-01-11T16:02:00Z" } GET CURRENT USER (Check Auth Status) GET /api/v1/auth/me Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Response: { "success": true, "httpStatus": "OK", "message": "User retrieved successfully", "action_time": "2025-01-11T18:00:00", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "displayName": "Alex Johnson", "firstName": "Alex", "lastName": "Johnson", "email": "alex@example.com", "phoneNumber": "+255712345678", "profilePictureUrl": "https://storage.example.com/...", "bio": "Fashion lover | Shopaholic | Event creator ๐Ÿ”ฅ", "birthDate": "1995-06-15", "isEmailVerified": true, "isPhoneVerified": true, "hasPassword": true, "authProviders": ["PHONE", "GOOGLE"], "onboardingComplete": true, "onboardingStep": "COMPLETE", "interests": [ { "id": "cat_001", "name": "Fashion" }, { "id": "cat_003", "name": "Beauty & Cosmetics" } ], "createdAt": "2025-01-11T15:20:00", "updatedAt": "2025-01-11T15:27:00" } } ONBOARDING STATUS CHECK GET /api/v1/onboarding/status Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Response (Incomplete): { "success": true, "message": "Onboarding in progress", "data": { "onboardingComplete": false, "currentStep": "PROFILE_SETUP", "completedSteps": ["SIGNUP", "NAME_BIRTHDATE"], "remainingSteps": ["PROFILE_SETUP", "INTERESTS"], "progress": 50 }, "timestamp": "2025-01-11T15:23:00Z" } Response (Complete): { "success": true, "message": "Onboarding complete", "data": { "onboardingComplete": true, "currentStep": "COMPLETE", "completedSteps": ["SIGNUP", "NAME_BIRTHDATE", "PROFILE_SETUP", "INTERESTS"], "remainingSteps": [], "progress": 100 }, "timestamp": "2025-01-11T15:27:00Z" } ERROR RESPONSE FORMAT (Standard) All error responses follow GlobeFailureResponseBuilder structure: { "success": false, "httpStatus": "BAD_REQUEST", "message": "Human readable error message", "action_time": "2025-01-11T15:20:00", "data": { "code": "ERROR_CODE", "field": "fieldName", "details": "Additional details if any", "suggestion": "What user can do" } } Common Error Codes Code HTTP Status Description VALIDATION_ERROR BAD_REQUEST Request validation failed INVALID_OTP BAD_REQUEST OTP code is incorrect OTP_EXPIRED BAD_REQUEST OTP has expired INVALID_CREDENTIALS UNAUTHORIZED Wrong password TOKEN_EXPIRED UNAUTHORIZED Access token expired REFRESH_TOKEN_EXPIRED UNAUTHORIZED Refresh token expired UNAUTHORIZED UNAUTHORIZED Not authenticated FORBIDDEN FORBIDDEN Not allowed USER_NOT_FOUND NOT_FOUND User doesn't exist ACCOUNT_EXISTS CONFLICT Account already exists USERNAME_TAKEN CONFLICT Username is taken PHONE_ALREADY_REGISTERED CONFLICT Phone belongs to another account PHONE_TEMPORARILY_RESERVED CONFLICT Phone claimed but unverified ACCOUNT_LOCKED LOCKED Too many failed attempts RESEND_COOLDOWN TOO_MANY_REQUESTS Wait before resending OTP RATE_LIMITED TOO_MANY_REQUESTS Too many requests WEAK_PASSWORD UNPROCESSABLE_ENTITY Password doesn't meet requirements UNDERAGE UNPROCESSABLE_ENTITY User is under minimum age MIN_INTERESTS_REQUIRED UNPROCESSABLE_ENTITY Must have minimum interests MAX_INTERESTS_REACHED UNPROCESSABLE_ENTITY Maximum interests reached SERVER_ERROR INTERNAL_SERVER_ERROR Internal server error ENUMS REFERENCE OnboardingStep SIGNUP NAME_BIRTHDATE PROFILE_SETUP INTERESTS COMPLETE AuthProvider PHONE EMAIL GOOGLE APPLE OTP Purpose SIGNUP_VERIFICATION LOGIN_OTP PASSWORD_RESET EMAIL_VERIFICATION PHONE_VERIFICATION SUMMARY: What's New vs Existing New Endpoints POST /api/v1/auth/signup/initiate (replaces /register ) POST /api/v1/auth/signup/verify (replaces /verify-otp ) POST /api/v1/auth/signup/google POST /api/v1/auth/signup/apple PUT /api/v1/onboarding/name-birthdate GET /api/v1/onboarding/username/check POST /api/v1/onboarding/profile-picture PUT /api/v1/onboarding/profile-setup GET /api/v1/onboarding/interests/categories POST /api/v1/onboarding/interests POST /api/v1/onboarding/interests/skip GET /api/v1/onboarding/complete POST /api/v1/auth/login/password POST /api/v1/auth/login/otp/request POST /api/v1/auth/login/otp/verify POST /api/v1/auth/password/set GET /api/v1/auth/me GET /api/v1/onboarding/status Modified Endpoints POST /api/v1/auth/otp/resend (updated response) POST /api/v1/auth/token/refresh (same, just standardized response) POST /api/v1/auth/password/reset/request (updated from /psw-reset-otp ) POST /api/v1/auth/password/reset/confirm (updated from /reset-password ) Deprecated/Removed POST /api/v1/auth/register (replaced by /signup/initiate ) POST /api/v1/auth/login (replaced by password/OTP specific endpoints) INTEREST SYSTEM The interest system tracks user preferences through two methods: Explicit : User picks during onboarding (visible to user) Implicit : Silent tracking based on user behavior (invisible to user) Scores update silently in the background as users interact with content. Interest Scoring Logic Action Score Notes Explicitly selected (onboarding) +100 Base score for picked interests View post +1 Quick glance Like post +3 Shows interest Comment on post +5 Higher engagement Share post +7 Strong signal Save/Bookmark +5 Wants to revisit Follow user in category +10 Committed interest View product +2 Shopping interest Add to cart +8 Purchase intent Purchase product +15 Strongest signal View event +2 Curious Attend event +12 Committed Search term +4 Active seeking Time spent (per 30sec) +1 Passive engagement Scroll past quickly -1 Not interested Hide/Not interested -20 Explicit dislike Report content -30 Strong negative Score Decay : 10% reduction per week of no interaction (keeps recommendations fresh) Max Score : 1000 per category ADMIN: Interest Category Management Create Category POST /api/v1/admin/interests/categories Headers: Authorization: Bearer {admin_token} Request: { "name": "Fashion", "icon": "๐Ÿ‘—", "iconUrl": "https://cdn.example.com/icons/fashion.png", "color": "#FF6B6B", "keywords": ["clothes", "style", "outfit", "wear", "dress"], "displayOrder": 1, "isActive": true } Response: { "success": true, "message": "Category created successfully", "data": { "id": "cat_001", "name": "Fashion", "icon": "๐Ÿ‘—", "iconUrl": "https://cdn.example.com/icons/fashion.png", "color": "#FF6B6B", "keywords": ["clothes", "style", "outfit", "wear", "dress"], "displayOrder": 1, "isActive": true, "createdAt": "2025-01-11T16:00:00Z", "updatedAt": "2025-01-11T16:00:00Z" }, "timestamp": "2025-01-11T16:00:00Z" } List All Categories (Admin View) GET /api/v1/admin/interests/categories?includeInactive=true Response: { "success": true, "message": "Categories retrieved successfully", "data": { "categories": [ { "id": "cat_001", "name": "Fashion", "icon": "๐Ÿ‘—", "iconUrl": "https://cdn.example.com/icons/fashion.png", "color": "#FF6B6B", "keywords": ["clothes", "style", "outfit"], "displayOrder": 1, "isActive": true, "stats": { "usersExplicit": 15420, "usersImplicit": 28750, "totalEngagements": 89500, "postsTagged": 8920, "productsTagged": 3450, "shopsTagged": 234, "eventsTagged": 89 }, "createdAt": "2025-01-01T10:00:00Z", "updatedAt": "2025-01-10T14:30:00Z" }, { "id": "cat_002", "name": "Electronics", "icon": "๐Ÿ“ฑ", "iconUrl": "https://cdn.example.com/icons/electronics.png", "color": "#4ECDC4", "keywords": ["phone", "laptop", "gadget", "tech"], "displayOrder": 2, "isActive": true, "stats": { "usersExplicit": 12300, "usersImplicit": 31000, "totalEngagements": 125000, "postsTagged": 5600, "productsTagged": 8900, "shopsTagged": 456, "eventsTagged": 23 }, "createdAt": "2025-01-01T10:00:00Z", "updatedAt": "2025-01-10T14:30:00Z" } ], "totalCategories": 20, "activeCategories": 18, "inactiveCategories": 2 }, "timestamp": "2025-01-11T16:00:00Z" } Update Category PUT /api/v1/admin/interests/categories/{categoryId} Request: { "name": "Fashion & Style", "icon": "๐Ÿ‘—", "iconUrl": "https://cdn.example.com/icons/fashion-v2.png", "color": "#FF5252", "keywords": ["clothes", "style", "outfit", "wear", "dress", "apparel"], "isActive": true } Response: { "success": true, "message": "Category updated successfully", "data": { "id": "cat_001", "name": "Fashion & Style", "icon": "๐Ÿ‘—", "iconUrl": "https://cdn.example.com/icons/fashion-v2.png", "color": "#FF5252", "keywords": ["clothes", "style", "outfit", "wear", "dress", "apparel"], "displayOrder": 1, "isActive": true, "updatedAt": "2025-01-11T16:05:00Z" }, "timestamp": "2025-01-11T16:05:00Z" } Reorder Categories PUT /api/v1/admin/interests/categories/reorder Request: { "order": [ { "categoryId": "cat_003", "displayOrder": 1 }, { "categoryId": "cat_001", "displayOrder": 2 }, { "categoryId": "cat_002", "displayOrder": 3 } ] } Response: { "success": true, "message": "Categories reordered successfully", "data": { "updated": 3 }, "timestamp": "2025-01-11T16:10:00Z" } Deactivate Category (Soft Delete) DELETE /api/v1/admin/interests/categories/{categoryId} Response: { "success": true, "message": "Category deactivated successfully", "data": { "id": "cat_015", "name": "Outdated Category", "isActive": false, "deactivatedAt": "2025-01-11T16:15:00Z", "note": "Category hidden from users. Existing user data preserved." }, "timestamp": "2025-01-11T16:15:00Z" } Get Category Analytics GET /api/v1/admin/interests/categories/{categoryId}/analytics?period=30d Response: { "success": true, "message": "Analytics retrieved successfully", "data": { "categoryId": "cat_001", "categoryName": "Fashion", "period": "LAST_30_DAYS", "overview": { "totalUsers": 44170, "explicitUsers": 15420, "implicitUsers": 28750, "averageScore": 67.5, "totalEngagements": 89500 }, "trend": { "direction": "UP", "percentageChange": 12.5, "newUsersThisPeriod": 2340 }, "engagement": { "likes": 34000, "comments": 12500, "shares": 8900, "saves": 15600, "purchases": 3200 }, "topContent": { "topPosts": [ { "postId": "post_123", "engagements": 4500 }, { "postId": "post_456", "engagements": 3200 } ], "topProducts": [ { "productId": "prod_789", "sales": 234 }, { "productId": "prod_012", "sales": 189 } ] }, "demographics": { "ageGroups": [ { "range": "13-17", "percentage": 15 }, { "range": "18-24", "percentage": 35 }, { "range": "25-34", "percentage": 30 }, { "range": "35-44", "percentage": 12 }, { "range": "45+", "percentage": 8 } ] } }, "timestamp": "2025-01-11T16:20:00Z" } PUBLIC: Interest Categories Get Active Categories (Onboarding & Settings) GET /api/v1/interests/categories No auth required for onboarding, shows only active categories Response: { "success": true, "message": "Categories retrieved successfully", "data": { "categories": [ { "id": "cat_001", "name": "Fashion", "icon": "๐Ÿ‘—", "iconUrl": "https://cdn.example.com/icons/fashion.png", "color": "#FF6B6B" }, { "id": "cat_002", "name": "Electronics", "icon": "๐Ÿ“ฑ", "iconUrl": "https://cdn.example.com/icons/electronics.png", "color": "#4ECDC4" }, { "id": "cat_003", "name": "Beauty & Cosmetics", "icon": "๐Ÿ’„", "iconUrl": "https://cdn.example.com/icons/beauty.png", "color": "#FF69B4" }, { "id": "cat_004", "name": "Food & Drinks", "icon": "๐Ÿ”", "iconUrl": "https://cdn.example.com/icons/food.png", "color": "#F39C12" }, { "id": "cat_005", "name": "Sports & Fitness", "icon": "โšฝ", "iconUrl": "https://cdn.example.com/icons/sports.png", "color": "#2ECC71" }, { "id": "cat_006", "name": "Music & Dance", "icon": "๐ŸŽต", "iconUrl": "https://cdn.example.com/icons/music.png", "color": "#9B59B6" }, { "id": "cat_007", "name": "Home & Decor", "icon": "๐Ÿ ", "iconUrl": "https://cdn.example.com/icons/home.png", "color": "#E67E22" }, { "id": "cat_008", "name": "Tech & Gadgets", "icon": "๐Ÿ’ป", "iconUrl": "https://cdn.example.com/icons/tech.png", "color": "#3498DB" }, { "id": "cat_009", "name": "Travel", "icon": "โœˆ๏ธ", "iconUrl": "https://cdn.example.com/icons/travel.png", "color": "#1ABC9C" }, { "id": "cat_010", "name": "Gaming", "icon": "๐ŸŽฎ", "iconUrl": "https://cdn.example.com/icons/gaming.png", "color": "#8E44AD" }, { "id": "cat_011", "name": "Books & Reading", "icon": "๐Ÿ“š", "iconUrl": "https://cdn.example.com/icons/books.png", "color": "#D35400" }, { "id": "cat_012", "name": "Art & Design", "icon": "๐ŸŽจ", "iconUrl": "https://cdn.example.com/icons/art.png", "color": "#E74C3C" }, { "id": "cat_013", "name": "Health & Wellness", "icon": "๐Ÿง˜", "iconUrl": "https://cdn.example.com/icons/wellness.png", "color": "#27AE60" }, { "id": "cat_014", "name": "Automotive", "icon": "๐Ÿš—", "iconUrl": "https://cdn.example.com/icons/auto.png", "color": "#34495E" }, { "id": "cat_015", "name": "Pets & Animals", "icon": "๐Ÿพ", "iconUrl": "https://cdn.example.com/icons/pets.png", "color": "#F1C40F" }, { "id": "cat_016", "name": "Photography", "icon": "๐Ÿ“ท", "iconUrl": "https://cdn.example.com/icons/photo.png", "color": "#7F8C8D" }, { "id": "cat_017", "name": "Kids & Baby", "icon": "๐Ÿ‘ถ", "iconUrl": "https://cdn.example.com/icons/kids.png", "color": "#FFB6C1" }, { "id": "cat_018", "name": "Business & Finance", "icon": "๐Ÿ’ผ", "iconUrl": "https://cdn.example.com/icons/business.png", "color": "#2C3E50" }, { "id": "cat_019", "name": "Entertainment", "icon": "๐ŸŽฌ", "iconUrl": "https://cdn.example.com/icons/entertainment.png", "color": "#C0392B" }, { "id": "cat_020", "name": "DIY & Crafts", "icon": "๐Ÿ› ๏ธ", "iconUrl": "https://cdn.example.com/icons/diy.png", "color": "#16A085" } ], "selectionRules": { "minimum": 3, "recommended": 5, "maximum": 15, "canSkip": true } }, "timestamp": "2025-01-11T15:25:00Z" } USER: Interest Management Get My Interests GET /api/v1/interests/me Headers: Authorization: Bearer {access_token} Response: { "success": true, "message": "Interests retrieved successfully", "data": { "explicit": [ { "categoryId": "cat_001", "categoryName": "Fashion", "icon": "๐Ÿ‘—", "color": "#FF6B6B", "source": "ONBOARDING", "addedAt": "2025-01-11T15:26:00Z" }, { "categoryId": "cat_003", "categoryName": "Beauty & Cosmetics", "icon": "๐Ÿ’„", "color": "#FF69B4", "source": "SETTINGS", "addedAt": "2025-01-12T10:00:00Z" } ], "topImplicit": [ { "categoryId": "cat_002", "categoryName": "Electronics", "icon": "๐Ÿ“ฑ", "color": "#4ECDC4", "score": 87, "trend": "RISING" }, { "categoryId": "cat_006", "categoryName": "Music & Dance", "icon": "๐ŸŽต", "color": "#9B59B6", "score": 65, "trend": "STABLE" }, { "categoryId": "cat_009", "categoryName": "Travel", "icon": "โœˆ๏ธ", "color": "#1ABC9C", "score": 42, "trend": "FALLING" } ], "summary": { "explicitCount": 2, "implicitCount": 8, "topCategory": "Fashion", "feedPersonalization": "HIGH" } }, "timestamp": "2025-01-11T18:00:00Z" } Update My Explicit Interests PUT /api/v1/interests/me Headers: Authorization: Bearer {access_token} Request: { "categoryIds": ["cat_001", "cat_003", "cat_006", "cat_010", "cat_012"] } Response: { "success": true, "message": "Interests updated successfully", "data": { "explicit": [ { "categoryId": "cat_001", "categoryName": "Fashion" }, { "categoryId": "cat_003", "categoryName": "Beauty & Cosmetics" }, { "categoryId": "cat_006", "categoryName": "Music & Dance" }, { "categoryId": "cat_010", "categoryName": "Gaming" }, { "categoryId": "cat_012", "categoryName": "Art & Design" } ], "changes": { "added": ["cat_006", "cat_010", "cat_012"], "removed": [], "unchanged": ["cat_001", "cat_003"] }, "feedImpact": "Your feed will now show more Music, Gaming, and Art content" }, "timestamp": "2025-01-11T18:05:00Z" } Add Single Interest POST /api/v1/interests/me/{categoryId} Headers: Authorization: Bearer {access_token} Response: { "success": true, "message": "Interest added successfully", "data": { "categoryId": "cat_015", "categoryName": "Pets & Animals", "icon": "๐Ÿพ", "addedAt": "2025-01-11T18:10:00Z", "totalExplicit": 6 }, "timestamp": "2025-01-11T18:10:00Z" } Response (Max Reached): { "success": false, "message": "Maximum interests reached", "error": { "code": "MAX_INTERESTS_REACHED", "current": 15, "maximum": 15, "suggestion": "Remove an interest before adding a new one" }, "timestamp": "2025-01-11T18:10:00Z" } Remove Single Interest DELETE /api/v1/interests/me/{categoryId} Headers: Authorization: Bearer {access_token} Response: { "success": true, "message": "Interest removed successfully", "data": { "categoryId": "cat_015", "categoryName": "Pets & Animals", "removedAt": "2025-01-11T18:15:00Z", "totalExplicit": 5, "note": "You may still see some Pets & Animals content based on your activity" }, "timestamp": "2025-01-11T18:15:00Z" } Response (Minimum Required): { "success": false, "message": "Cannot remove interest", "error": { "code": "MIN_INTERESTS_REQUIRED", "current": 3, "minimum": 3, "suggestion": "Add another interest before removing this one" }, "timestamp": "2025-01-11T18:15:00Z" } Hide Content Category (Negative Signal) POST /api/v1/interests/me/{categoryId}/hide User explicitly says "not interested" - strong negative signal Headers: Authorization: Bearer {access_token} Response: { "success": true, "message": "You'll see less of this content", "data": { "categoryId": "cat_018", "categoryName": "Business & Finance", "action": "HIDDEN", "feedImpact": "Business & Finance content will be significantly reduced in your feed", "canUndo": true, "undoExpiry": "2025-01-11T18:30:00Z" }, "timestamp": "2025-01-11T18:20:00Z" } Unhide Content Category DELETE /api/v1/interests/me/{categoryId}/hide Response: { "success": true, "message": "Category unhidden", "data": { "categoryId": "cat_018", "categoryName": "Business & Finance", "action": "UNHIDDEN", "feedImpact": "Business & Finance content will appear normally based on your activity" }, "timestamp": "2025-01-11T18:25:00Z" } Get Hidden Categories GET /api/v1/interests/me/hidden Response: { "success": true, "message": "Hidden categories retrieved", "data": { "hidden": [ { "categoryId": "cat_018", "categoryName": "Business & Finance", "icon": "๐Ÿ’ผ", "hiddenAt": "2025-01-11T18:20:00Z" }, { "categoryId": "cat_014", "categoryName": "Automotive", "icon": "๐Ÿš—", "hiddenAt": "2025-01-05T12:00:00Z" } ], "totalHidden": 2 }, "timestamp": "2025-01-11T18:30:00Z" } INTERNAL: Silent Interest Tracking These are NOT public API endpoints. They are triggered internally when users interact with content. How It Works When user performs any action on content (post, product, shop, event), the system: Gets the category tags from that content Calculates score based on action type Updates user's implicit interest scores silently No API call from client needed Actions That Trigger Tracking User Action System Tracks Views post POST_VIEW on post's categories Likes post POST_LIKE on post's categories Comments on post POST_COMMENT on post's categories Shares post POST_SHARE on post's categories Saves post POST_SAVE on post's categories Views product PRODUCT_VIEW on product's category Adds to cart PRODUCT_CART on product's category Purchases PRODUCT_PURCHASE on product's category Views shop SHOP_VIEW on shop's categories Follows shop SHOP_FOLLOW on shop's categories Views event EVENT_VIEW on event's categories RSVPs to event EVENT_RSVP on event's categories Follows user USER_FOLLOW on user's primary categories Searches SEARCH on matched categories Scrolls past quickly SCROLL_PAST (negative) Clicks "Not interested" HIDE_CONTENT (strong negative) Content Must Have Category Tags For tracking to work, all content must be tagged: // Post { "id": "post_123", "content": "Check out my new outfit!", "categoryIds": ["cat_001"] // Fashion } // Product { "id": "prod_456", "name": "Wireless Earbuds", "categoryId": "cat_002" // Electronics } // Shop { "id": "shop_789", "name": "StyleHub", "categoryIds": ["cat_001", "cat_003"] // Fashion, Beauty } // Event { "id": "event_012", "title": "Summer Music Festival", "categoryIds": ["cat_006", "cat_019"] // Music, Entertainment } FEED: Using Interests for Recommendations Get Personalized Feed GET /api/v1/feed?page=1&size=20 Feed algorithm uses interest scores to rank content Response includes personalization info: { "success": true, "data": { "posts": [ { "id": "post_123", "content": "...", "author": { "...": "..." }, "categoryIds": ["cat_001"], "relevanceScore": 0.95, "relevanceReason": "EXPLICIT_INTEREST" }, { "id": "post_456", "content": "...", "author": { "...": "..." }, "categoryIds": ["cat_002"], "relevanceScore": 0.87, "relevanceReason": "HIGH_IMPLICIT_SCORE" }, { "id": "post_789", "content": "...", "author": { "...": "..." }, "categoryIds": ["cat_015"], "relevanceScore": 0.45, "relevanceReason": "TRENDING" } ], "personalization": { "status": "ACTIVE", "basedOn": { "explicitInterests": 5, "implicitInterests": 8, "followedAccounts": 23 } }, "pagination": { "...": "..." } } } SUMMARY: Interest System Endpoints Admin Endpoints Method Endpoint Description POST /api/v1/admin/interests/categories Create category GET /api/v1/admin/interests/categories List all (with stats) PUT /api/v1/admin/interests/categories/{id} Update category DELETE /api/v1/admin/interests/categories/{id} Deactivate category PUT /api/v1/admin/interests/categories/reorder Reorder categories GET /api/v1/admin/interests/categories/{id}/analytics Get analytics Public Endpoints Method Endpoint Description GET /api/v1/interests/categories Get active categories User Endpoints (Requires Auth) Method Endpoint Description GET /api/v1/interests/me Get my interests PUT /api/v1/interests/me Update all explicit POST /api/v1/interests/me/{id} Add single interest DELETE /api/v1/interests/me/{id} Remove single interest POST /api/v1/interests/me/{id}/hide Hide category DELETE /api/v1/interests/me/{id}/hide Unhide category GET /api/v1/interests/me/hidden Get hidden list Internal (No Public API) Silent tracking triggered by user actions Score calculation and decay Feed personalization New Database Fields Needed AccountEntity // Existing fields to keep private UUID id; private String userName; // Public @handle (changeable) private String phoneNumber; private String email; private String password; private String firstName; private String lastName; private String middleName; private String bio; private String location; private Boolean isVerified; private Boolean isEmailVerified; private Boolean isPhoneVerified; private boolean twoFactorEnabled; private String twoFactorSecret; private boolean locked; private String lockedReason; private LocalDateTime createdAt; private LocalDateTime editedAt; private Set roles; private List profilePictureUrls; private boolean isBucketCreated; // NEW fields to add private String systemUsername; // Internal identifier (never changes) - "usr_550e8400e29b41d4" private LocalDate birthDate; // For age gating private String displayName; // Full display name "Alex Johnson" private String authProvider; // PHONE, EMAIL, GOOGLE, APPLE (primary signup method) private String googleId; // Google OAuth ID private String appleId; // Apple OAuth ID private String onboardingStep; // SIGNUP, NAME_BIRTHDATE, PROFILE_SETUP, INTERESTS, COMPLETE private Boolean onboardingComplete; // Quick check flag private Boolean hasPassword; // Whether user set a password private LocalDateTime phoneClaimedAt; // When phone was first set (for claim expiry) private LocalDateTime phoneVerifiedAt;// When phone was verified private LocalDateTime emailClaimedAt; // When email was first set private LocalDateTime emailVerifiedAt;// When email was verified New Entities InterestCategory (Admin managed) - id (UUID) - name (String) - icon (String) - emoji - iconUrl (String) - image URL - color (String) - hex color - keywords (List) - for auto-tagging - displayOrder (Integer) - isActive (Boolean) - createdAt (LocalDateTime) - updatedAt (LocalDateTime) UserInterest (User's interests) - id (UUID) - userId (UUID) - categoryId (UUID) - source (Enum: ONBOARDING, SETTINGS, IMPLICIT) - score (Integer) - 0 to 1000 - isExplicit (Boolean) - isHidden (Boolean) - lastInteractionAt (LocalDateTime) - createdAt (LocalDateTime) - updatedAt (LocalDateTime) InterestEvent (Tracking log - optional, for analytics) - id (UUID) - userId (UUID) - categoryId (UUID) - actionType (Enum) - weight (Integer) - sourceType (Enum: POST, PRODUCT, SHOP, EVENT, USER, SEARCH) - sourceId (UUID) - createdAt (LocalDateTime) JWT Token Structure Access Token Payload: { "sub": "usr_550e8400e29b41d4", "tokenType": "ACCESS", "iat": 1736605200, "exp": 1736608800 } Refresh Token Payload: { "sub": "usr_550e8400e29b41d4", "tokenType": "REFRESH", "iat": 1736605200, "exp": 1768141200 } Note: sub (subject) uses systemUsername , NOT userName . This allows username changes without token invalidation. Username Change Flow (No Logout Required) PUT /api/v1/profile/update-basic-info Request: { "userName": "newusername" } Response (Success): { "success": true, "httpStatus": "OK", "message": "Username updated successfully", "action_time": "2025-01-11T16:00:00", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "newusername", "previousUserName": "oldusername", "note": "Your profile URL is now: app.com/@newusername" } } JWT token remains valid because it uses systemUsername which hasn't changed. System Username Generation Generated automatically at account creation: public String generateSystemUsername(UUID userId) { // Take first 16 chars of UUID (without hyphens) String shortId = userId.toString().replace("-", "").substring(0, 16); return "usr_" + shortId; } // Example: // UUID: 550e8400-e29b-41d4-a716-446655440000 // systemUsername: usr_550e8400e29b41d4 Rules: Prefix: usr_ Length: 20 characters total Characters: lowercase alphanumeric only Unique: derived from UUID Never displayed to users Never changeable DEVICE TRUST & LOGIN SECURITY Overview The system tracks user devices and applies a sliding trust window: New device โ†’ OTP required before password login Inactive device (30+ days) โ†’ OTP required (re-verification) Trusted active device โ†’ Password login allowed Suspicious activity โ†’ OTP required Trust Sliding Window Device Activity Timeline: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ NOW โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ–ผ Day 0 Day 25 Day 35 (Login) (Last use) (Login attempt) โ”‚ โ”‚ โ”‚ โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€ TRUSTED โ”€โ”€โ”€โ”€โ–บโ”‚ โ”‚ โ”‚ โ”‚โ—„โ”€โ”€โ”€ 30 DAY GAP โ”€โ”€โ”€โ–บโ”‚ โ”‚ โ”‚ โ”‚ Device now UNTRUSTED โ”‚ OTP required to re-trust Login Decision Matrix Device Status Last Activity Password Set? Action New (unknown) Never Yes OTP first โ†’ then password โ†’ trust device New (unknown) Never No OTP only โ†’ trust device Known trusted < 30 days Yes Password only โœ… Known trusted < 30 days No Auto-login or OTP Known trusted > 30 days Yes OTP first โ†’ then password โ†’ re-trust Known trusted > 30 days No OTP โ†’ re-trust Known untrusted Any Any OTP required Any (suspicious) Any Any OTP required + security alert Suspicious Activity Triggers 3+ failed password attempts Login from new country/region Multiple devices logging in simultaneously Unusual login time (if pattern established) IP address on blocklist LOGIN FLOWS (Updated with Device Trust) Password Login - Full Flow POST /api/v1/auth/login/password Request: { "identifier": "alexvibes", "password": "MySecurePass123!", "deviceInfo": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "deviceType": "MOBILE_IOS", "appVersion": "1.2.0" } } Response (Success - Trusted Device): { "success": true, "httpStatus": "OK", "message": "Login successful", "action_time": "2025-01-11T16:00:00", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "requiresOtp": false, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "displayName": "Alex Johnson", "profilePictureUrl": "https://storage.example.com/...", "onboardingComplete": true }, "device": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "trusted": true, "trustExpiresAt": "2025-02-10T16:00:00" } } } Response (New/Untrusted Device - OTP Required): { "success": true, "httpStatus": "OK", "message": "New device detected. Please verify with OTP.", "action_time": "2025-01-11T16:00:00", "data": { "requiresOtp": true, "otpReason": "NEW_DEVICE", "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpSentTo": "+255*****678", "otpMethod": "SMS", "expiresAt": "2025-01-11T16:10:00", "device": { "deviceId": "xyz789-new-device", "deviceName": "Chrome on Windows", "isNew": true }, "securityNote": "We detected a login from a new device. For your security, please verify with OTP." } } Response (Inactive Device - OTP Required): { "success": true, "httpStatus": "OK", "message": "Device inactive for 30+ days. Please verify with OTP.", "action_time": "2025-01-11T16:00:00", "data": { "requiresOtp": true, "otpReason": "INACTIVE_DEVICE", "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpSentTo": "a***@example.com", "otpMethod": "EMAIL", "expiresAt": "2025-01-11T16:10:00", "device": { "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "lastActiveAt": "2024-12-01T10:00:00", "inactiveDays": 41 }, "securityNote": "This device hasn't been used in 41 days. Please verify it's you." } } Verify Device OTP (Step 2 for untrusted devices) POST /api/v1/auth/login/verify-device Request: { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpCode": "123456", "trustDevice": true, "deviceInfo": { "deviceId": "xyz789-new-device", "deviceName": "Chrome on Windows", "deviceType": "WEB_BROWSER" } } Response (Success): { "success": true, "httpStatus": "OK", "message": "Device verified and trusted", "action_time": "2025-01-11T16:02:00", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "systemUsername": "usr_550e8400e29b41d4", "userName": "alexvibes", "displayName": "Alex Johnson", "profilePictureUrl": "https://storage.example.com/...", "onboardingComplete": true }, "device": { "deviceId": "xyz789-new-device", "deviceName": "Chrome on Windows", "trusted": true, "trustExpiresAt": "2025-02-10T16:02:00" } } } Login Without Trusting Device (One-time access) Request: { "tempToken": "eyJhbGciOiJIUzI1NiIs...", "otpCode": "123456", "trustDevice": false } Response: { "success": true, "httpStatus": "OK", "message": "Login successful (device not saved)", "action_time": "2025-01-11T16:02:00", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "tokenType": "Bearer", "expiresIn": 3600, "user": { ... }, "device": { "trusted": false, "note": "This device will require OTP on next login" } } } DEVICE MANAGEMENT Get My Devices GET /api/v1/auth/devices Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Response: { "success": true, "httpStatus": "OK", "message": "Devices retrieved successfully", "action_time": "2025-01-11T18:00:00", "data": { "devices": [ { "id": "device-uuid-1", "deviceId": "abc123-device-fingerprint", "deviceName": "iPhone 15 Pro", "deviceType": "MOBILE_IOS", "isTrusted": true, "isCurrentDevice": true, "lastActiveAt": "2025-01-11T18:00:00", "lastIpAddress": "196.41.xxx.xxx", "lastLocation": "Dar es Salaam, Tanzania", "createdAt": "2025-01-01T10:00:00" }, { "id": "device-uuid-2", "deviceId": "xyz789-device-fingerprint", "deviceName": "Chrome on MacBook", "deviceType": "WEB_BROWSER", "isTrusted": true, "isCurrentDevice": false, "lastActiveAt": "2025-01-10T14:30:00", "lastIpAddress": "196.41.xxx.xxx", "lastLocation": "Dar es Salaam, Tanzania", "createdAt": "2024-12-15T08:00:00" }, { "id": "device-uuid-3", "deviceId": "old-device-fingerprint", "deviceName": "Samsung Galaxy S21", "deviceType": "MOBILE_ANDROID", "isTrusted": false, "isCurrentDevice": false, "lastActiveAt": "2024-11-20T09:00:00", "lastIpAddress": "41.59.xxx.xxx", "lastLocation": "Arusha, Tanzania", "createdAt": "2024-06-01T12:00:00", "untrustedReason": "Inactive for 52 days" } ], "totalDevices": 3, "trustedDevices": 2 } } Remove/Logout Device DELETE /api/v1/auth/devices/{deviceId} Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Response: { "success": true, "httpStatus": "OK", "message": "Device removed successfully", "action_time": "2025-01-11T18:05:00", "data": { "removedDeviceId": "device-uuid-3", "removedDeviceName": "Samsung Galaxy S21", "note": "This device has been logged out and will require OTP to login again" } } Logout All Other Devices POST /api/v1/auth/devices/logout-all Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Request: { "password": "MySecurePass123!", "keepCurrentDevice": true } Response: { "success": true, "httpStatus": "OK", "message": "All other devices logged out", "action_time": "2025-01-11T18:10:00", "data": { "devicesLoggedOut": 2, "currentDeviceKept": true, "note": "All other devices will require OTP to login again" } } Rename Device PUT /api/v1/auth/devices/{deviceId} Request: { "deviceName": "My Work Laptop" } Response: { "success": true, "httpStatus": "OK", "message": "Device renamed successfully", "action_time": "2025-01-11T18:15:00", "data": { "deviceId": "device-uuid-2", "deviceName": "My Work Laptop", "previousName": "Chrome on MacBook" } } LOGIN EVENTS/HISTORY Get Login History GET /api/v1/auth/login-history?page=1&size=20 Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIs... Response: { "success": true, "httpStatus": "OK", "message": "Login history retrieved", "action_time": "2025-01-11T18:20:00", "data": { "events": [ { "id": "event-uuid-1", "loginMethod": "PASSWORD", "deviceName": "iPhone 15 Pro", "deviceType": "MOBILE_IOS", "ipAddress": "196.41.xxx.xxx", "location": "Dar es Salaam, Tanzania", "status": "SUCCESS", "requiresOtp": false, "createdAt": "2025-01-11T16:00:00" }, { "id": "event-uuid-2", "loginMethod": "OTP", "deviceName": "Chrome on Windows", "deviceType": "WEB_BROWSER", "ipAddress": "196.41.xxx.xxx", "location": "Dar es Salaam, Tanzania", "status": "SUCCESS", "requiresOtp": true, "otpReason": "NEW_DEVICE", "createdAt": "2025-01-11T14:30:00" }, { "id": "event-uuid-3", "loginMethod": "PASSWORD", "deviceName": "Unknown Device", "deviceType": "WEB_BROWSER", "ipAddress": "102.89.xxx.xxx", "location": "Lagos, Nigeria", "status": "FAILED_PASSWORD", "requiresOtp": false, "createdAt": "2025-01-10T23:45:00", "securityAlert": true } ], "pagination": { "page": 1, "size": 20, "totalElements": 45, "totalPages": 3 }, "securitySummary": { "totalLogins30Days": 28, "failedAttempts30Days": 2, "uniqueDevices30Days": 3, "uniqueLocations30Days": 1 } } } SECURITY ALERTS Get Security Alerts GET /api/v1/auth/security-alerts Response: { "success": true, "httpStatus": "OK", "message": "Security alerts retrieved", "action_time": "2025-01-11T18:25:00", "data": { "alerts": [ { "id": "alert-uuid-1", "type": "SUSPICIOUS_LOGIN", "severity": "HIGH", "title": "Login attempt from new location", "description": "Someone tried to login from Lagos, Nigeria", "deviceName": "Unknown Device", "ipAddress": "102.89.xxx.xxx", "location": "Lagos, Nigeria", "status": "UNREAD", "actionTaken": "BLOCKED", "createdAt": "2025-01-10T23:45:00", "actions": [ { "action": "DISMISS", "label": "This was me" }, { "action": "SECURE_ACCOUNT", "label": "Secure my account" } ] }, { "id": "alert-uuid-2", "type": "NEW_DEVICE_LOGIN", "severity": "MEDIUM", "title": "New device added", "description": "Chrome on Windows was added to your account", "deviceName": "Chrome on Windows", "location": "Dar es Salaam, Tanzania", "status": "READ", "actionTaken": null, "createdAt": "2025-01-11T14:30:00", "actions": [ { "action": "DISMISS", "label": "This was me" }, { "action": "REMOVE_DEVICE", "label": "Remove this device" } ] } ], "unreadCount": 1, "totalAlerts": 2 } } Dismiss Security Alert POST /api/v1/auth/security-alerts/{alertId}/dismiss Request: { "action": "DISMISS", "feedback": "This was me logging in from a friend's device" } Response: { "success": true, "httpStatus": "OK", "message": "Alert dismissed", "action_time": "2025-01-11T18:30:00", "data": { "alertId": "alert-uuid-1", "status": "DISMISSED", "unreadCount": 0 } } New Database Entities for Device Trust UserDevice - id (UUID) - userId (UUID) FK โ†’ AccountEntity - deviceId (String) - fingerprint from client (unique per user) - deviceName (String) - "iPhone 15 Pro", "Chrome on Windows" - deviceType (Enum) - MOBILE_IOS, MOBILE_ANDROID, WEB_BROWSER, DESKTOP_APP - userAgent (String) - lastIpAddress (String) - lastLocation (String) - isTrusted (Boolean) - trustExpiresAt (LocalDateTime) - 30 days sliding window - lastActiveAt (LocalDateTime) - createdAt (LocalDateTime) - updatedAt (LocalDateTime) Indexes: - (userId, deviceId) UNIQUE - (userId, isTrusted) - (lastActiveAt) LoginEvent - id (UUID) - userId (UUID) FK โ†’ AccountEntity - deviceId (UUID) FK โ†’ UserDevice (nullable for failed attempts) - loginMethod (Enum) - PASSWORD, OTP_SMS, OTP_EMAIL, GOOGLE, APPLE - ipAddress (String) - location (String) - status (Enum) - SUCCESS, FAILED_PASSWORD, FAILED_OTP, BLOCKED, REQUIRES_OTP - requiresOtp (Boolean) - otpReason (Enum) - NEW_DEVICE, INACTIVE_DEVICE, SUSPICIOUS, MANUAL - createdAt (LocalDateTime) Indexes: - (userId, createdAt) - (userId, status) - (ipAddress, createdAt) - for rate limiting SecurityAlert - id (UUID) - userId (UUID) FK โ†’ AccountEntity - loginEventId (UUID) FK โ†’ LoginEvent (nullable) - type (Enum) - SUSPICIOUS_LOGIN, NEW_DEVICE_LOGIN, FAILED_ATTEMPTS, PASSWORD_CHANGED, etc. - severity (Enum) - LOW, MEDIUM, HIGH, CRITICAL - title (String) - description (String) - deviceName (String) - ipAddress (String) - location (String) - status (Enum) - UNREAD, READ, DISMISSED, ACTIONED - actionTaken (String) - createdAt (LocalDateTime) - readAt (LocalDateTime) - dismissedAt (LocalDateTime) Indexes: - (userId, status) - (userId, createdAt) Device Trust Configuration # application.yml device: trust: enabled: true window-days: 30 # Trust window duration max-devices-per-user: 10 # Max trusted devices auto-cleanup-days: 90 # Remove inactive devices after login: security: max-failed-attempts: 5 # Before temporary lock lock-duration-minutes: 30 # Temporary lock duration suspicious-countries: [XX, YY] # Countries requiring extra verification alert-on-new-country: true # Send alert on new country login New NextGate Auth & Onboarding Flow UI (DEPRECATED) 1. Sign Up Flow Path A: Phone User Enter phone number โ”‚ โ–ผ Verify OTP โ”‚ โ–ผ Enter age & name โ”‚ โ–ผ Pick username โ”‚ โ–ผ Pick interests (min 3) โ”‚ โ–ผ Profile setup (optional) โ”‚ โ–ผ Home Feed ๐ŸŽ‰ (device registered) โ”€โ”€ Post-onboarding โ”€โ”€ Soft nudge to add email (banner only, never blocking) Path B: Email User Enter email address โ”‚ โ–ผ Verify OTP โ”‚ โ–ผ Enter age & name โ”‚ โ–ผ Pick username โ”‚ โ–ผ Verify phone number โ† REQUIRED (Identity) โ”‚ โ–ผ Pick interests (min 3) โ”‚ โ–ผ Profile setup (optional) โ”‚ โ–ผ Home Feed ๐ŸŽ‰ (device registered) Path C: Google / Apple OAuth Tap Google / Apple โ”‚ โ–ผ OAuth authorize โ”‚ โ–ผ Enter age & name (pre-filled from OAuth) โ”‚ โ–ผ Pick username (suggestions from name) โ”‚ โ–ผ Verify phone number โ† REQUIRED (Identity) โ”‚ โ–ผ Pick interests (min 3) โ”‚ โ–ผ Profile setup (optional) (profile pic pre-filled) โ”‚ โ–ผ Home Feed ๐ŸŽ‰ (device registered) 2. Onboarding Steps Detail Step Summary # Step Applies To Endpoint 0 OTP / OAuth entry All /auth/start + /auth/verify or /auth/login/oauth 1 Age & name All /auth/onboarding/age 2 Username All /auth/onboarding/username 3 Contact verify (PHONE) Email + OAuth only /auth/onboarding/contact/initiate + /contact/verify 4 Interests All /auth/onboarding/interests 5 Profile All /auth/onboarding/profile Contact Verification Rule Signup Method Required Contact Reason Phone Email (optional, post-onboarding) Phone = real identity, M-Pesa already linked Email Phone (blocking) Needed for M-Pesa payments Google / Apple Phone (blocking) OAuth gives email, phone still required for payments Onboarding Step Sequence Phone user: INITIATED โ†’ OTP_VERIFIED โ†’ AGE_VERIFIED โ†’ USERNAME_SET โ†’ INTERESTS_SELECTED โ†’ PROFILE_COMPLETED โ†’ COMPLETED Email / OAuth user: INITIATED โ†’ OTP_VERIFIED โ†’ AGE_VERIFIED โ†’ USERNAME_SET โ†’ CONTACT_VERIFIED โ†’ INTERESTS_SELECTED โ†’ PROFILE_COMPLETED โ†’ COMPLETED Token Expiry Token Expiry Purpose onboardingToken 7 days Active onboarding flow, resets each step onboardingRefreshToken 30 days Safety net if token expires mid-flow accessToken Configured Normal app access refreshToken Configured Access token rotation 3. Contact Verification โ€” Inline UX (3 states, 1 page) STATE A โ€” Input โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๐Ÿ“ฑ Add your phone number โ”‚ โ”‚ โ”‚ โ”‚ Your email jโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com โœ… โ”‚ โ”‚ โ”‚ โ”‚ We need your phone for: โ”‚ โ”‚ โ€ข Offline notifications โ”‚ โ”‚ โ€ข Account security & recovery โ”‚ โ”‚ โ”‚ โ”‚ Phone: [+255______________] โ”‚ โ”‚ [Send Code โ†’] โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ STATE B โ€” OTP Input โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Enter code sent to โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50 โ”‚ โ”‚ โ”‚ โ”‚ [_] [_] [_] [_] [_] [_] โ”‚ โ”‚ โ”‚ โ”‚ [Edit number] Resend (60s) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ STATE C โ€” Edit (tapping [Edit number] transforms inline) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ New number: [+255______________] โ”‚ โ”‚ [Send new code โ†’] [Cancel] โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Previous OTP invalidated immediately on edit. No page navigation โ€” single page, three inline states. 4. Login Flow Identifier Detection Input pattern Detected as Starts with + Phone Contains @ + domain Email Otherwise Username Complete Login Flow โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Login Screen โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ–ผ Google/ Phone/Email Username Apple โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ–ผ OAuth Detect Find account โ”‚ account โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ Has password? Show masked โ”‚ โ”‚ OTP destination โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ No Yes โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ โ”‚ [OTP] [Password] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ–ผ โ”‚ โ”‚ OTP Screen Password โ”‚ โ”‚ โ”‚ Screen โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ–ผ Device Check Device Check OTP Screen โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ–ผ (see ยง5) (see ยง5) Device Check โ”‚ โ–ผ (see ยง5) 5. Device Trust Check After Authentication User Authenticated โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Check Device โ”‚ โ”‚ โ€ข Device ID โ”‚ โ”‚ โ€ข Fingerprint โ”‚ โ”‚ โ€ข IP / Location โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ Is Device Known? โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ” โ–ผ โ–ผ Yes No โ”‚ โ”‚ โ–ผ โ–ผ Home ๐ŸŽ‰ Login Method? โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ–ผ โ–ผ โ–ผ OTP Password OAuth โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ–ผ Home ๐ŸŽ‰ OTP OTP Required Required โ”‚ โ”‚ โ–ผ โ–ผ Verify Verify โ”‚ โ”‚ โ–ผ โ–ผ Register Device โ”‚ โ–ผ Home ๐ŸŽ‰ Device Trust Rules Login Method New Device Action Reason OTP None OTP = already verified Password Require OTP Password could be stolen OAuth Require OTP Token could be compromised 6. Risk Scoring Signal Low Risk High Risk Location Same country New country Device Similar OS Different OS IP Normal VPN / Proxy Account Active Dormant Time Normal hours Unusual hours Action by Risk Risk Score โ”‚ โ”œโ”€โ”€ Low Risk โ”€โ”€โ–บ Soft verify (email link, approve from known device) โ”‚ โ””โ”€โ”€ High Risk โ”€โ”€โ–บ Phone OTP required 7. UI Screens Sign Up Screen โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Create Account ๐Ÿš€ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Phone or Email โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Continue โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”€โ”€ or โ”€โ”€ โ”‚ โ”‚ โ”‚ โ”‚ [Google] [Apple] โ”‚ โ”‚ โ”‚ โ”‚ Already have account? Login โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ OTP Verification Screen โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Verify OTP ๐Ÿ” โ”‚ โ”‚ โ”‚ โ”‚ Enter code sent to: โ”‚ โ”‚ +255 712 345 678 โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ Resend code (0:45) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Age & Name Screen โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Let's get started ๐Ÿ“… โ”‚ โ”‚ โ”‚ โ”‚ First name: โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ John โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ Last name: โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Doe โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ Date of birth: โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ 1999 / 05 / 15 โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Continue โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Username Screen โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Pick Username ๐Ÿท๏ธ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ @johndoe99 โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โœ… @johndoe99 is available โ”‚ โ”‚ โ”‚ โ”‚ Suggestions: โ”‚ โ”‚ @johndoe_tz @jdoe99 @the_john โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Continue โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Contact Verification Screen (Email/OAuth users only) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Add your phone number ๐Ÿ“ฑ โ”‚ โ”‚ โ”‚ โ”‚ Your email jโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com โœ… โ”‚ โ”‚ โ”‚ โ”‚ Needed for: โ”‚ โ”‚ โ€ข SMS reminders โ”‚ โ”‚ โ€ข Account security and recovery โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ +255 _____________________ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Send Code โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Interests Screen โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Pick Your Interests ๐ŸŽฏ โ”‚ โ”‚ (at least 3) โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Fashion โ”‚ โ”‚ Tech โ”‚ โ”‚ Music โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Sports โ”‚ โ”‚ Food โ”‚ โ”‚Gaming โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Travel โ”‚ โ”‚ Art โ”‚ โ”‚Fitnessโ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Continue โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Profile Setup Screen โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Complete Profile โœจ โ”‚ โ”‚ (optional) โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ๐Ÿ“ท โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ Add photo โ”‚ โ”‚ โ”‚ โ”‚ Display name: โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ John Doe โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ Bio: โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Music lover | Dar es Salaam ๐Ÿ‡น๐Ÿ‡ฟโ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Complete โ”‚ โ”‚ Skip for now โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Login Screen โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Welcome Back ๐Ÿ‘‹ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Phone, email, or username โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Continue โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”€โ”€ or โ”€โ”€ โ”‚ โ”‚ โ”‚ โ”‚ [Google] [Apple] โ”‚ โ”‚ โ”‚ โ”‚ Don't have account? Sign up โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Login Method Choice (if password exists) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ How do you want to login? โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ๐Ÿ“ฑ Send me OTP โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ๐Ÿ”‘ Use password โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ OTP Destination Choice (username login) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Send OTP to: โ”‚ โ”‚ โ”‚ โ”‚ โ—‹ โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข45 โ”‚ โ”‚ โ—‹ jโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Send OTP โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ New Device Detected โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ๐Ÿ†• New device detected โ”‚ โ”‚ โ”‚ โ”‚ For your security, verify it's โ”‚ โ”‚ you with a one-time code. โ”‚ โ”‚ โ”‚ โ”‚ โ—‹ โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข45 โ”‚ โ”‚ โ—‹ jโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Send OTP โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 8. Security Settings (Post-onboarding) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Security Settings ๐Ÿ”’ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ Phone โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ +255 712 XXX XXX โœ… Verifiedโ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ Email โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Not added [Add now] โ”‚ โ”‚ โ† phone users see this โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ ๐Ÿ’ก Add email for ticket delivery โ”‚ โ”‚ โ”‚ โ”‚ Password โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Not set [Add] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ ๐Ÿ’ก Optional extra security โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ Linked Accounts โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Google Not linked [Link] โ”‚ โ”‚ โ”‚ โ”‚ Apple Not linked [Link] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 9. Summary Sign Up Path Steps Phone OTP โ†’ Age โ†’ Username โ†’ Interests โ†’ Profile โ†’ Home Email OTP โ†’ Age โ†’ Username โ†’ Verify Phone โ†’ Interests โ†’ Profile โ†’ Home OAuth Authorize โ†’ Age โ†’ Username โ†’ Verify Phone โ†’ Interests โ†’ Profile โ†’ Home Login Has Password? Options No OTP only Yes Choose: OTP or Password Device Verification Login Method New Device Action OTP None (OTP = verification) Password OTP required OAuth OTP required Post-Onboarding Nudges (Phone users) Feature Requires Email? Behaviour Browse feed No Free Buy ticket No Free Receive ticket via email Yes Nudge to add email Account recovery Yes Nudge to add email NextGate Authentication Specification v1.0 (DEPRECATED) 1. Overview Approach: Passwordless-first, device-trusted, risk-aware authentication. Principle Implementation Passwordless default OTP-based, password optional (add later in settings) Device trust Hardware-bound keys (mobile), fingerprint (web) Risk-based Dynamic verification based on risk score Age-gated 18+ full access, tiered restrictions below 2. Sign Up Flow โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Enter phone/email โ”‚ โ”‚ OR tap Google/Apple โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ–ผ โ–ผ Phone/Email OAuth โ”‚ โ”‚ โ–ผ โ”‚ Verify OTP โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Enter Birthdate (Age Gate) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ–ผ โ–ผ โ–ผ 18+ 13-17 Under 13 โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ–ผ Continue Restricted Block โ”‚ + Parent โ”‚ โ”‚ Consent โ–ผ โ”‚ โ”‚ "Come back โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ later" โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Pick Username (@handle) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Select Interests (min 3) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Profile Setup (optional skip) โ”‚ โ”‚ โ€ข Display name โ”‚ โ”‚ โ€ข Photo โ”‚ โ”‚ โ€ข Bio โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ Home ๐ŸŽ‰ (Device registered) 3. Login Flow โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Enter phone/email/username โ”‚ โ”‚ OR tap Google/Apple โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ–ผ โ–ผ Identifier OAuth โ”‚ โ”‚ โ–ผ โ”‚ Detect type: โ”‚ โ€ข + prefix โ†’ Phone โ”‚ โ€ข @domain โ†’ Email โ”‚ โ€ข else โ†’ Username โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ Find account โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ” โ”‚ โ–ผ โ–ผ โ”‚ Has PWD? No PWD โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚Choose: โ”‚ โ”‚ โ”‚ โ”‚โ€ข OTP โ”‚ โ”‚ โ”‚ โ”‚โ€ข PWD โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ If username โ”‚ โ”‚ โ”‚ login: show โ”‚ โ”‚ โ”‚ masked OTP โ”‚ โ”‚ โ”‚ destination โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ Verify OTP/PWD โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ Device Check โ”‚ (see section 5) OTP Destination (Username Login) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Send OTP to: โ”‚ โ”‚ โ”‚ โ”‚ โ—‹ โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข45 โ”‚ โ”‚ โ—‹ jโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com โ”‚ โ”‚ โ”‚ โ”‚ [Send OTP] โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ If only one exists โ†’ skip choice, send directly 4. Login Method Summary Has Password? Login Options No OTP only (passwordless) Yes Choose: OTP or Password Login Method New Device Handling OTP None needed โ€” OTP is verification Password OTP required on new device OAuth OTP required on new device 5. Device Trust Platform Strategy Platform Method Trust Level iOS Secure Enclave key pair โญโญโญโญโญ High Android StrongBox/TEE Keystore โญโญโญโญโญ High Web Fingerprint + session key โญโญโญ Medium Mobile Device Registration First App Launch โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Generate key pair in Secure Enclave โ”‚ โ”‚ โ€ข Private key: NEVER leaves device โ”‚ โ”‚ โ€ข Public key: sent to server โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ Device registered โœ… Login with Device Verification โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 1. Client: GET /auth/challenge โ”‚ โ”‚ Server returns: { nonce: "xyz" } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 2. Client: Sign nonce with โ”‚ โ”‚ hardware private key โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 3. Client: POST /auth/login โ”‚ โ”‚ { signature, deviceId, nonce } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 4. Server: Verify signature with โ”‚ โ”‚ stored public key โ”‚ โ”‚ โ€ข Valid โ†’ trusted device โœ… โ”‚ โ”‚ โ€ข Invalid โ†’ block โŒ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Why Attacker Fails Attacker has: โœ… Password, โœ… DeviceId, โœ… Nonce Attacker needs: โŒ Private key (locked in victim's hardware) Result: โŒ Cannot forge signature โ†’ Attack fails 6. Risk Scoring Signals & Weights Signal Low (0) Medium High Location Same city Same country (+10) New country (+25) Device Known (0) Similar OS (+10) New OS (+20) IP Normal ISP (0) Different ISP (+10) VPN/TOR (+30) Time Normal hours (0) Unusual (+10) 2-5 AM (+15) Failed attempts None (0) 1-2 (+10) 3+ (+25) Velocity Normal (0) Multiple (+15) Rapid (+30) Device signature Valid (-20) Missing (+15) Invalid (+40) Thresholds & Actions Score Risk Level Action 0-30 ๐ŸŸข Low Allow 31-60 ๐ŸŸก Medium Soft verify (email link) 61-85 ๐Ÿ”ด High Phone OTP required 86-100 โ›” Critical Block + alert user Impossible Travel Last login: Dar es Salaam at 10:00 AM This login: London at 10:30 AM Distance: 7,500 km in 30 min = impossible โ†’ +40 points โ†’ likely compromised 7. Age Restriction Tiers Age Access Level 18+ Full access 13-17 Restricted (no purchases, filtered content) Under 13 Blocked (COPPA) Blocked User Handling User blocked (underage) โ”‚ โ–ผ Tries again with same phone/email โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ System checks: โ”‚ โ”‚ โ€ข Phone/email in blocked list? โ”‚ โ”‚ โ€ข Device fingerprint matches? โ”‚ โ”‚ โ€ข Same IP? โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ Block signup "Cannot register at this time" 8. Username Rules Change Limits (Anti-Fraud) Account Age Allowed Changes Day 0 (today) 5 changes 1-30 days 1 per month 1-12 months 1 per month 12+ months 1 per year SYSTEM accounts Never Account Types Type Examples Username Change NORMAL Regular users Limited (above) SYSTEM @nextgate, @admin, @support Never VERIFIED @nike, @cocacola Requires approval 9. Session Management Sign Out Options Action What It Does Requires Sign out Current device only Nothing Sign out others All except current OTP/Password Sign out all Everything OTP/Password Active Sessions View โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๐Ÿ“ฑ iPhone 14 Pro โ”‚ โ”‚ Dar es Salaam โ€ข Active now โ”‚ โ”‚ This device [โ—] โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ ๐Ÿ’ป Chrome on Windows โ”‚ โ”‚ Nairobi โ€ข 2 hours ago โ”‚ โ”‚ [Sign out] โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ [Sign out other devices] โ”‚ โ”‚ [Sign out all devices] โš ๏ธ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 10. Security Settings โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Security Settings ๐Ÿ”’ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Phone: +255 712 โ€ขโ€ขโ€ข456 โœ… Verified โ”‚ โ”‚ Email: jโ€ขโ€ขโ€ขโ€ข@email.com โœ… Verified โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Password: Not set [Add] โ”‚ โ”‚ ๐Ÿ’ก Optional extra security โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Linked Accounts: โ”‚ โ”‚ Google: Not linked [Link] โ”‚ โ”‚ Apple: Not linked [Link] โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 11. API Endpoints Auth - Signup Endpoint Purpose POST /auth/signup/initiate Start signup (phone/email) POST /auth/signup/verify-otp Verify OTP POST /auth/signup/age Submit birthdate POST /auth/signup/username Set username POST /auth/signup/interests Select interests POST /auth/signup/profile Complete profile (optional) Auth - Login Endpoint Purpose POST /auth/login/initiate Start login POST /auth/login/otp Login with OTP POST /auth/login/password Login with password GET /auth/challenge Get nonce for device signing Auth - Device Endpoint Purpose POST /auth/device/register Register device (public key) POST /auth/device/verify Verify new device OTP GET /auth/devices List trusted devices DELETE /auth/devices/{id} Revoke device Auth - Session Endpoint Purpose GET /auth/sessions List active sessions POST /auth/sign-out Current device POST /auth/sign-out-others All except current POST /auth/sign-out-all Everything 12. Database Entities New Entities Entity Purpose DeviceKey Hardware-bound public keys UserSession Active sessions LoginAttempt Risk scoring data BlockedUser Blocked identifiers/devices InterestCategory Admin-managed interests UserInterest User selections UsernameChangeHistory Track changes AccountEntity Changes Field Change password Make nullable birthDate Add displayName Add accountType Add (NORMAL, SYSTEM, VERIFIED) accountTier Add (FULL, RESTRICTED, MINOR) authProvider Add (PHONE, EMAIL, GOOGLE, APPLE) onboardingStep Add usernameLastChangedAt Add usernameChangeCount Add 13. Enums AuthProvider: PHONE, EMAIL, GOOGLE, APPLE AccountType: NORMAL, SYSTEM, VERIFIED AccountTier: FULL, RESTRICTED, MINOR DevicePlatform: IOS, ANDROID, WEB TrustLevel: HIGH, MEDIUM, LOW RiskLevel: LOW, MEDIUM, HIGH, CRITICAL 14. Quick Reference Onboarding Steps 1. Signup (phone/email/OAuth) 2. Verify OTP (if phone/email) 3. Birthdate (age gate) 4. Username 5. Interests (min 3) 6. Profile (optional) Device Verification Matrix Login Method Known Device New Device OTP โ†’ Home โ†’ Home (OTP is verification) Password โ†’ Home โ†’ OTP required โ†’ Home OAuth โ†’ Home โ†’ OTP required โ†’ Home Risk Score Quick Reference 0-30: Allow 31-60: Soft verify 61-85: Phone OTP 86-100: Block + alert 15. Complete Device & Auth Flow (Top to Bottom) When Does What Happen? Action When Where Device Registration First app launch (before any auth) Mobile only Web Session Init First visit (before any auth) Web only Risk Scoring After credentials verified, before home Login only Device Verification OTP After risk score (if needed) Login only (new device + password/OAuth) Master Flow Chart โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ APP/WEB FIRST LAUNCH โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ–ผ โ–ผ Mobile App Web Browser โ”‚ โ”‚ โ–ผ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Generate key pair in โ”‚ โ”‚ Generate session ID โ”‚ โ”‚ Secure Enclave/TEE โ”‚ โ”‚ + collect fingerprint โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Store private key โ”‚ โ”‚ Store in memory โ”‚ โ”‚ (hardware, never โ”‚ โ”‚ (ephemeral) โ”‚ โ”‚ leaves device) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Show Login/Signup โ”‚ โ”‚ Screen โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ–ผ โ–ผ SIGNUP LOGIN โ”‚ โ”‚ โ–ผ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ SIGNUP FLOW โ”‚ โ”‚ LOGIN FLOW โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 1. Enter phone/email/OAuth โ”‚ โ”‚ 1. Enter identifier (phone/email/username)โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ OR tap OAuth โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 2. Verify OTP (if not OAuth) โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 2. GET /auth/challenge โ—„โ”€โ”€ GET NONCE โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ Server returns { nonce: "xyz" } โ”‚ โ”‚ 3. Enter birthdate (age gate) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ 3. Sign nonce with hardware key (mobile) โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ OR attach fingerprint (web) โ”‚ โ”‚ 18+ <18 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (block/ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ restrict) โ”‚ โ”‚ 4. Find account โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 4. Pick username โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ Has password? No password โ”‚ โ”‚ 5. Select interests (min 3) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ Show choice: โ”‚ โ”‚ โ”‚ 6. Profile setup (optional) โ”‚ โ”‚ โ€ข OTP โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ€ข Password โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 7. Register device with โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ server (send public key) โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 5. POST /auth/login with: โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ€ข credentials (OTP or password) โ”‚ โ”‚ HOME ๐ŸŽ‰ โ”‚ โ”‚ โ€ข deviceId โ”‚ โ”‚ (Device trusted) โ”‚ โ”‚ โ€ข nonce โ”‚ โ”‚ โ”‚ โ”‚ โ€ข signature โ—„โ”€โ”€ SIGNED NONCE โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ 6. Server validates BOTH: โ”‚ โ”‚ โ€ข Credentials โœ“ โ”‚ โ”‚ โ€ข Signature โœ“ (if known device) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ DEVICE CHECK (After Auth) โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Is device known? โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ Known Unknown โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Login method? โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ OTP PWD/OAuth โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Calculate risk โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Low/Med High โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (0-60) (61+) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Soft verify Phone OTP โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (email link) required โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Register new device โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (send public key) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ HOME ๐ŸŽ‰ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Challenge-Response: Detailed Flow โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ CHALLENGE-RESPONSE FLOW โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ STEP 1: User enters identifier (before submitting credentials) โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” GET /auth/challenge โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Client โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ โ”‚ Server โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ Generate nonce โ”‚ โ”‚ (random, expires 60s) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” { nonce: "xyz123" } โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Client โ”‚ โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ Server โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ STEP 2: Client signs nonce (mobile only) โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ signature = sign( โ”‚ โ”‚ โ”‚ โ”‚ nonce + timestamp, โ”‚ โ”‚ โ”‚ โ”‚ privateKey โ—„โ”€โ”€ from Secure Enclave โ”‚ โ”‚ โ”‚ โ”‚ ) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ STEP 3: Submit login with signature โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” POST /auth/login โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Client โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ โ”‚ Server โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ { โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ identifier: "user@mail.com", โ”‚ โ”‚ โ”‚ otp: "123456", โ”‚ โ”‚ โ”‚ deviceId: "dev_abc", โ”‚ โ”‚ โ”‚ nonce: "xyz123", โ”‚ โ”‚ โ”‚ signature: "abc123..." โ”‚ โ”‚ โ”‚ } โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ STEP 4: Server validates โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ 1. Nonce valid? โ”‚ โ”‚ โ”‚ โ”‚ (not expired, โ”‚ โ”‚ โ”‚ โ”‚ not reused) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ 2. Credentials? โ”‚ โ”‚ โ”‚ โ”‚ (OTP/password) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ โ”‚ 3. Signature? โ”‚ โ”‚ โ”‚ โ”‚ (verify with โ”‚ โ”‚ โ”‚ โ”‚ stored pubkey) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ All pass Any fail โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ Continue Reject โ”‚ โ”‚ to device login โ”‚ โ”‚ check โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ When Challenge Happens: Summary Scenario Challenge? Signature Validated? Signup โŒ No โŒ No (device not registered yet) Login - Known device (mobile) โœ… Yes โœ… Yes (must match) Login - Known device (web) โœ… Yes ๐ŸŸก Fingerprint checked Login - Unknown device โœ… Yes โŒ No pubkey stored yet Request/Response Example 1. Get Challenge: GET /auth/challenge Response: { "nonce": "ch_7f8a9b2c3d4e5f6g", "expiresIn": 60 } 2. Login with Signature: POST /auth/login { "identifier": "alex@email.com", "otp": "123456", "deviceId": "dev_iphone14_abc123", "nonce": "ch_7f8a9b2c3d4e5f6g", "signature": "MEUCIQD2k3n...(base64 signed data)...", "timestamp": "2026-01-12T10:30:00Z" } Signup vs Login: What Happens Where Step Signup Login Device key generation โœ… Before auth (app launch) โœ… Before auth (app launch) OTP verification โœ… To verify phone/email โœ… As login method OR device verify Age gate โœ… After OTP โŒ Not needed Username โœ… Required โŒ Not needed Interests โœ… Required โŒ Not needed Risk scoring โŒ Not needed (new account) โœ… After credentials verified Device verification OTP โŒ Not needed (first device) โœ… If new device + high risk Device registration โœ… End of onboarding โœ… After device verification Risk Scoring: When & How Login credentials verified โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Collect signals: โ”‚ โ”‚ โ€ข IP / Location โ”‚ โ”‚ โ€ข Device info โ”‚ โ”‚ โ€ข User agent โ”‚ โ”‚ โ€ข Timestamp โ”‚ โ”‚ โ€ข Login history โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Calculate score: โ”‚ โ”‚ Location: +25 โ”‚ โ”‚ Device: +20 โ”‚ โ”‚ IP: +0 โ”‚ โ”‚ Time: +10 โ”‚ โ”‚ Velocity: +0 โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”‚ Total: 55 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Score: 55 = MEDIUM โ”‚ โ”‚ Action: Soft verifyโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 16. Logout / Sign Out Options Action What It Does Requires Sign out End current session Nothing Sign out other devices End all except current OTP or Password Sign out all devices End everything OTP or Password Flow User taps "Sign out" โ”‚ โ”œโ”€โ”€ "Sign out" (this device) โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ Revoke current token โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ†’ Login screen โ”‚ โ”œโ”€โ”€ "Sign out other devices" โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ Verify (OTP or Password) โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ Revoke all tokens except current โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ "Other devices signed out" โœ… โ”‚ โ””โ”€โ”€ "Sign out all devices" โ”‚ โ–ผ Verify (OTP or Password) โ”‚ โ–ผ Revoke ALL tokens (including current) โ”‚ โ–ผ โ†’ Login screen Session Management Screen โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Active Sessions ๐Ÿ”’ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ ๐Ÿ“ฑ iPhone 14 Pro โ”‚ โ”‚ Dar es Salaam โ€ข Active now โ”‚ โ”‚ This device [โ—] โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ’ป Chrome on Windows โ”‚ โ”‚ Nairobi โ€ข 2 hours ago โ”‚ โ”‚ [Sign out] โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ“ฑ Samsung Galaxy S23 โ”‚ โ”‚ Mombasa โ€ข Yesterday โ”‚ โ”‚ [Sign out] โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ [Sign out other devices] โ”‚ โ”‚ โ”‚ โ”‚ [Sign out all devices] โš ๏ธ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Sign Out Endpoints Endpoint Purpose POST /auth/sign-out Current device POST /auth/sign-out-others All except current POST /auth/sign-out-all Everything DELETE /auth/sessions/{id} Specific session 18. Industry Comparison NextGate vs Major Platforms Feature NextGate Instagram Twitter/X WhatsApp Banking Apps Passwordless default โœ… Yes โŒ No โŒ No โœ… Yes ๐ŸŸก Some Hardware-bound keys โœ… Yes โŒ No โŒ No โŒ No โœ… Yes Risk-based auth โœ… Yes ๐ŸŸก Basic ๐ŸŸก Basic โŒ No โœ… Yes Device trust โœ… Advanced ๐ŸŸก Basic ๐ŸŸก Basic ๐ŸŸก Basic โœ… Advanced Impossible travel detection โœ… Yes ๐ŸŸก Limited ๐ŸŸก Limited โŒ No โœ… Yes Session management โœ… Full โœ… Full โœ… Full ๐ŸŸก Limited โœ… Full Age verification โœ… Tiered ๐ŸŸก Basic ๐ŸŸก Basic โŒ No โœ… Yes Username change limits โœ… Smart ๐ŸŸก 14 days ๐ŸŸก Limited โŒ N/A โŒ N/A 2FA options โœ… OTP โœ… OTP/App ๐Ÿ’ฐ Paid โŒ No โœ… Multiple Where We Stand โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ NextGate Auth vs Industry โ”‚ โ”‚ โ”‚ โ”‚ Social Apps (Instagram, Twitter): AHEAD โœ… โ”‚ โ”‚ Messaging Apps (WhatsApp, Telegram): EQUAL ๐ŸŸก โ”‚ โ”‚ Banking/Fintech: EQUAL ๐ŸŸก โ”‚ โ”‚ Big Tech (Google, Apple): BEHIND โŒ โ”‚ โ”‚ โ”‚ โ”‚ For Social Commerce Platform: EXCELLENT ๐ŸŽฏ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Our Advantages Over Advantage Instagram/Twitter Hardware-bound device keys, passwordless default WhatsApp Multi-identifier login, risk scoring, age gates Basic apps Challenge-response auth, impossible travel detection Future Improvements (v2) Feature Impact Effort Passkeys/WebAuthn +0.5 rating Medium Backup codes +0.2 rating Low Breach monitoring +0.2 rating Low ML anomaly detection +0.3 rating High Rating: 8.5/10 โญ Verdict: Enterprise-grade auth for a social commerce platform. Better than most social apps, equal to fintech. Version: 1.0 Status: Ready for implementation PONA AUTH V3 NextGate โ€” PONA Auth Flow V3 (Progressive ยท Onboarding ยท Native ยท Access) Version 3.0 โ€” The Complete Authentication Specification Phone-first. Passwordless by default. One flow. No walls. Trust earned progressively. Philosophy One entry point โ€” phone number only, always One auth system โ€” no separate lite or hard auth flows Passwordless by default โ€” password is an optional enhancement set later Progressive onboarding โ€” primary is mandatory, secondary collected only when a resource needs it One token type โ€” access token carries onboarding flags Persistent identity โ€” device remembers who you are, you never type your number twice Channel choice โ€” passwordless users pick where they receive their OTP Token Types Token Lifespan Purpose Issued At checkToken 5 mins Signed phone carrier โ€” binds all auth actions to one account /auth/check tempToken 10 mins OTP handshake only /auth/start , /onboarding/email/initiate onboardingToken 7 days Primary flow only โ€” unlocks name and age steps only After OTP verified, primary incomplete accessToken 1hr (no password) / 7 days (with password) Full session, carries onboarding flags After primary complete refreshToken 30 days Silent refresh, password users only, rotated on use After password login What "Primary Complete" Means Three requirements. All three done before access token is issued. No skip. No cancel. โœ… Phone verified via OTP โœ… First name + Last name set โœ… Date of birth set (age calculated โ†’ account tier assigned) Account Tiers โ€” Set at Age Step Age Tier What It Means Under 13 Blocked Account deleted. Phone blocklisted. Cannot return until 13th birthday. 13 โ€” 17 Restricted Age-restricted content hidden. Some commerce limited. 18+ Full No restrictions. Onboarding Flags (Inside Access Token) Derived from actual account data. No separate database column needed. Flag Means primaryComplete Phone verified + name set + date of birth set username Real username chosen โ€” not a system temp one email Email submitted AND verified via OTP profilePic At least one profile picture uploaded interests At least 3 interests selected bio Bio text written Access Token Shape { "sub": "su_uuid", "flags": { "primaryComplete": true, "username": false, "email": false, "profilePic": false, "interests": false, "bio": false }, "exp": "2026-04-01T12:00:00Z" } Resource Permission Matrix Feature Needs Primary Needs Secondary Browse events / listings โŒ No auth โ€” React / like โœ… nothing extra Buy ticket or product โœ… nothing extra Share listing โœ… nothing extra Comment publicly โœ… username Follow someone โœ… username Send a message โœ… username Create an event โœ… username + email Open a shop โœ… username + email Sell a product โœ… username + email Withdraw money โœ… username + email + profilePic Age-restricted content โœ… must be 18+ nothing extra Secondary Field Priority Order Backend returns missing fields one at a time in this order. User never sees all missing fields at once. 1 โ€” username (needed for almost all social features) 2 โ€” email (needed for commerce and trust) 3 โ€” profilePic (needed for high-trust actions) 4 โ€” bio (rarely hard-required) 5 โ€” interests (feed personalization, almost never hard-required) Auth Method Validation Every auth endpoint validates the user has the method they are trying to use. Endpoint Validation /auth/login/password Account must have password set /auth/login/oauth Google Google must be linked to this account /auth/login/oauth Apple Apple must be linked to this account /auth/password/forgot/initiate Account must have password set /auth/passwordless/channels Always allowed /auth/start OTP Always allowed โ€” passwordless available to everyone OTP Channel Selection Passwordless users with email set can choose where to receive their OTP. Frontend never passes the raw email or phone โ€” only the channel type enum. Channel Availability Rules Channel Available When PHONE Always โ€” phone is primary, always verified EMAIL Only when email is set AND verified on the account Action Codes โ€” Complete Reference Action Code What Frontend Does REGISTER New user โ€” show registration intro CONTINUE_ONBOARDING Returning user, primary incomplete โ€” resume LOGIN Account ready โ€” show auth method options RESTART_AUTH Token expired โ€” back to phone entry SELECT_CHANNEL Multiple OTP channels โ€” show picker PROCEED_TO_OTP Single channel only โ€” skip picker, go straight to OTP USE_OTP Wrong auth method chosen โ€” switch to OTP RETRY_OTP Wrong OTP โ€” error on same screen RESEND_OTP OTP expired โ€” activate resend WAIT Rate limited โ€” show countdown ACCOUNT_BLOCKED Under 13 โ€” show blocked screen COLLECT_USERNAME Username needed COLLECT_EMAIL Email needed โ€” submit then OTP verify COLLECT_PROFILE_PIC Profile picture needed COLLECT_INTERESTS Interests needed COLLECT_BIO Bio needed PROCEED All steps done โ€” retry original action Response Shapes Success { "success": true, "message": "Human readable message", "action": "NEXT_ACTION_OR_NULL", "data": { } } Error โ€” HTTP 422 { "success": false, "message": "Human readable message", "action": "NEXT_ACTION_CODE", "context": "what_user_was_trying_to_do", "data": { } } Response Examples /auth/check โ€” New User { "success": true, "message": "Phone number not registered", "action": "REGISTER", "data": { "exists": false, "checkToken": null } } /auth/check โ€” Existing User Ready { "success": true, "message": "Welcome back", "action": "LOGIN", "data": { "exists": true, "checkToken": "eyJ...", "primaryComplete": true, "maskedPhone": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78", "authMethods": { "passwordless": true, "password": true, "google": true, "apple": false } } } /auth/check โ€” Primary Incomplete { "success": true, "message": "Continue setting up your account", "action": "CONTINUE_ONBOARDING", "data": { "exists": true, "checkToken": "eyJ...", "primaryComplete": false, "maskedPhone": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78" } } /auth/passwordless/channels โ€” Multiple Channels { "success": true, "message": "Choose where to receive your code", "action": "SELECT_CHANNEL", "data": { "channels": [ { "type": "PHONE", "masked": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78", "isPrimary": true }, { "type": "EMAIL", "masked": "jโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com", "isPrimary": false } ] } } /auth/passwordless/channels โ€” Single Channel Only { "success": true, "message": "Sending code to your phone", "action": "PROCEED_TO_OTP", "data": { "channels": [ { "type": "PHONE", "masked": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78", "isPrimary": true } ] } } /auth/start โ€” OTP Sent { "success": true, "message": "Verification code sent", "action": null, "data": { "tempToken": "eyJ...", "maskedDestination": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78", "channel": "PHONE", "expiresInSeconds": 120, "resendAvailableAfterSeconds": 60 } } /auth/verify โ€” Primary Incomplete { "success": true, "message": "Phone verified. Let us set up your account.", "action": "COLLECT_PRIMARY", "data": { "onboardingToken": "eyJ...", "nextStep": "name" } } /auth/verify โ€” Primary Already Complete { "success": true, "message": "Welcome back!", "action": null, "data": { "accessToken": "eyJ...", "onboarding": { "primaryComplete": true, "username": true, "email": false, "profilePic": false, "interests": false, "bio": false } } } /auth/onboarding/age โ€” Blocked Underage { "success": false, "message": "You must be at least 13 years old to use NextGate", "action": "ACCOUNT_BLOCKED", "context": "underage", "data": { "unblockDate": "2027-06-15" } } /auth/onboarding/age โ€” Primary Complete { "success": true, "message": "Welcome to NextGate!", "action": null, "data": { "accessToken": "eyJ...", "accountTier": "FULL", "onboarding": { "primaryComplete": true, "username": false, "email": false, "profilePic": false, "interests": false, "bio": false } } } OTP Wrong { "success": false, "message": "Incorrect OTP code", "action": "RETRY_OTP", "context": "otp_verify", "data": { "attemptsRemaining": 2 } } OTP Expired { "success": false, "message": "OTP has expired", "action": "RESEND_OTP", "context": "otp_expired", "data": { "resendAvailable": true, "resendCooldownSeconds": 0 } } Rate Limited { "success": false, "message": "Too many attempts. Please wait.", "action": "WAIT", "context": "rate_limited", "data": { "retryAfterSeconds": 120 } } Wrong Auth Method { "success": false, "message": "This account does not use password login", "action": "USE_OTP", "context": "password_login", "data": { "availableMethods": ["passwordless", "google"] } } Secondary Gate โ€” Multiple Missing { "success": false, "message": "A couple of things needed before you can create events", "action": "COLLECT_USERNAME", "context": "create_event", "data": { "currentMissing": "username", "allMissing": ["username", "email"], "stepsRemaining": 2 } } Secondary Step Done โ€” Next Signalled { "success": true, "message": "Username set. One more step.", "action": "COLLECT_EMAIL", "context": "create_event", "data": { "accessToken": "eyJ...", "nextMissing": "email", "stepsRemaining": 1, "onboarding": { "primaryComplete": true, "username": true, "email": false, "profilePic": false, "interests": false, "bio": false } } } All Secondary Done โ€” Proceed { "success": true, "message": "All done. Creating your event now.", "action": "PROCEED", "context": "create_event", "data": { "accessToken": "eyJ...", "stepsRemaining": 0, "onboarding": { "primaryComplete": true, "username": true, "email": true, "profilePic": false, "interests": false, "bio": false } } } Forgot Password โ€” Reset Complete { "success": true, "message": "Password updated. All other sessions signed out.", "action": null, "data": { "accessToken": "eyJ..." } } Flow Diagrams FLOW 1 โ€” App Open with Stored Accounts โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ App opens โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ Read device secure storage for stored accounts list โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ NO ACCOUNTS ACCOUNTS FOUND โ”‚ โ”‚ โ–ผ โ–ผ Show clean phone Count stored accounts entry screen โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” ONE MULTIPLE โ”‚ โ”‚ โ–ผ โ–ผ Auto-call Show account /auth/check picker screen in background User taps one โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ /auth/check called for that identifier โ”‚ โ–ผ Show personalized welcome screen with auth method buttons FLOW 2 โ€” Auth Check (Entry Point) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ POST /auth/check โ”‚ โ”‚ { "identifier": "+255712345678" } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ Valid phone format? โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” NO YES โ”‚ โ”‚ โ–ผ โ–ผ 422 Look up in database Invalid โ”‚ phone โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ NOT FOUND FOUND โ”‚ โ”‚ โ–ผ โ–ผ action: REGISTER Phone verified? checkToken: null โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ” NO YES โ”‚ โ”‚ โ–ผ โ–ผ Release phone Primary complete? from orphan โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ” action: REGISTER NO YES โ”‚ โ”‚ โ–ผ โ–ผ action: action: LOGIN CONTINUE_ authMethods ONBOARDING returned โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ checkToken issued containing { identifier } stored to device on success FLOW 3 โ€” New User Registration action: REGISTER from /auth/check ............................................ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ POST /auth/start โ”‚ โ”‚ { "checkToken": "eyJ...", "channel": "PHONE" } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ Phone extracted from checkToken Partial account created OTP sent via SMS tempToken issued โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ POST /auth/verify โ”‚ โ”‚ { "tempToken": "eyJ...", "otp": "123456" } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ Phone verified Primary incomplete ONBOARDING TOKEN issued App locked to primary screens โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ–ผ โ–ผ POST /auth/ POST /auth/ onboarding/name onboarding/age { firstName, { birthDate } lastName } โ”‚ โ”‚ โ–ผ โ–ผ Under 13? โ†’ BLOCKED New onboarding 13-17 โ†’ RESTRICTED token returned 18+ โ†’ FULL Continue to age โ”‚ โ–ผ PRIMARY COMPLETE ACCESS TOKEN issued Identifier + name + avatar saved to device storage User lands on feed โœ“ FLOW 4 โ€” Existing User, Passwordless Login action: LOGIN, authMethods.passwordless: true User picks OTP option ............................................ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ POST /auth/passwordless/channels โ”‚ โ”‚ { "checkToken": "eyJ..." } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ Backend checks account channels โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ ONE CHANNEL MULTIPLE CHANNELS (phone only) (phone + email) โ”‚ โ”‚ โ–ผ โ–ผ action: action: SELECT_CHANNEL PROCEED_TO_OTP Show channel picker Skip picker User picks PHONE or EMAIL โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ POST /auth/start โ”‚ โ”‚ { "checkToken": "eyJ...", "channel": "PHONE" } โ”‚ โ”‚ or { "checkToken": "eyJ...", "channel": "EMAIL" } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ Backend extracts actual phone or email internally from account Sends OTP to chosen channel tempToken issued โ”‚ โ–ผ POST /auth/verify { tempToken, otp } โ”‚ โ–ผ OTP valid. Primary complete. ACCESS TOKEN issued. Device storage entry updated. User lands on feed โœ“ FLOW 5 โ€” Existing User, Password Login action: LOGIN, authMethods.password: true User picks password option ............................................ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ POST /auth/login/password โ”‚ โ”‚ { "checkToken": "eyJ...", "password": "..." } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ Account found from checkToken โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” NO PASSWORD HAS PASSWORD โ”‚ โ”‚ โ–ผ โ–ผ 422 Password verified action: USE_OTP Risk assessed availableMethods โ”‚ returned โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ KNOWN DEVICE UNKNOWN DEVICE โ”‚ โ”‚ โ–ผ โ–ผ ACCESS TOKEN Device OTP sent issued Verify device directly ACCESS TOKEN issued FLOW 6 โ€” OAuth Login action: LOGIN, authMethods.google: true User picks Google ............................................ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ POST /auth/login/oauth โ”‚ โ”‚ { "checkToken": "eyJ...", โ”‚ โ”‚ "provider": "GOOGLE", "code": "..." } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ Account found from checkToken Google linked to account? โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” NO YES โ”‚ โ”‚ โ–ผ โ–ผ 422 Google identity confirmed action: USE_OTP Profile data pre-filled availableMethods from Google returned โ”‚ Primary complete? โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” NO YES โ”‚ โ”‚ โ–ผ โ–ผ ONBOARDING TOKEN ACCESS TOKEN collect age issued โœ“ FLOW 7 โ€” Forgot Password Only shown when authMethods.password: true ............................................ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ POST /auth/password/forgot/initiate โ”‚ โ”‚ { "checkToken": "eyJ..." } โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ Account has password? โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” NO YES โ”‚ โ”‚ โ–ผ โ–ผ 422 OTP sent to phone action: USE_OTP tempToken issued โ”‚ โ–ผ POST /auth/password/forgot/verify-otp { tempToken, otp } โ”‚ โ–ผ OTP verified resetToken issued (10 mins, single use) โ”‚ โ–ผ POST /auth/password/forgot/reset { resetToken, newPassword, confirmPassword } โ”‚ โ–ผ Password updated All other sessions revoked ACCESS TOKEN issued User logged in โœ“ FLOW 8 โ€” Secondary Onboarding (Progressive) User tries to create an event Needs: username + email username: false โ† first missing email: false ............................................ 422 from resource guard action: COLLECT_USERNAME allMissing: ["username", "email"] stepsRemaining: 2 ............................................ Frontend: "2 steps โ€” Step 1 of 2" POST /onboarding/username Bearer { "username": "joshsakweli" } โ”‚ โ–ผ Username saved New accessToken issued action: COLLECT_EMAIL stepsRemaining: 1 ............................................ Frontend: "Step 2 of 2 โ€” Add email" POST /onboarding/email/initiate Bearer { "email": "josh@qbitspark.com" } โ”‚ โ–ผ OTP sent to email tempToken returned nextAction: VERIFY_EMAIL โ”‚ โ–ผ POST /onboarding/email/verify Bearer { "tempToken": "eyJ...", "otp": "123456" } โ”‚ โ–ผ Email verified New accessToken issued action: PROCEED stepsRemaining: 0 โ”‚ โ–ผ Frontend retries create event Passes โœ“ FLOW 9 โ€” Wrong Number, Changing During Registration User typed wrong number OTP sent. User clicks "Change number" Before OTP verified โ€” just restart ............................................ POST /auth/check { correct number } โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ NOT IN DB ALREADY IN DB โ”‚ โ”‚ โ–ผ โ–ผ Fresh Phone verified? registration โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ” continues NO YES โ”‚ โ”‚ โ–ผ โ–ผ Release phone Primary complete? from orphan โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ” New user NO YES flow โ”‚ โ”‚ CONTINUE_ "Number has account. ONBOARDING Login instead?" โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” LOGIN DIFFERENT โ”‚ NUMBER โ–ผ โ–ผ Login flow /auth/check again FLOW 10 โ€” Returning User, Token Expired App opened. Access token expired. ............................................ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ HAS PASSWORD NO PASSWORD โ”‚ โ”‚ โ–ผ โ–ผ Has refresh token? /auth/check auto-called โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” from stored identifier YES NO โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ–ผ Passwordless channel check Silent Show OTP sent to chosen channel refresh login /auth/verify ACCESS screen Primary complete โ†’ ACCESS TOKEN TOKEN directly, no onboarding shown โœ“ issued โœ“ Client-Side Persistent Identity This is a frontend-only feature. Zero backend changes required. What Gets Stored on Device โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Stored after every successful login โ”‚ โ”‚ โ”‚ โ”‚ identifier โ†’ "+255712345678" โ”‚ โ”‚ maskedPhone โ†’ "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78" โ”‚ โ”‚ displayName โ†’ "Joshua Sakweli" โ”‚ โ”‚ avatarUrl โ†’ "https://..." โ”‚ โ”‚ lastLoginAt โ†’ "2026-04-01T10:00:00Z" โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ NEVER store: โœ— Access tokens โœ— Refresh tokens โœ— Passwords or OTPs โœ— Full unmasked phone number in plain text Storage Location by Platform Platform Storage Method Android EncryptedSharedPreferences โ€” hardware-backed encryption iOS Keychain โ€” secure enclave Web localStorage โ€” for non-sensitive display data only, never tokens Stored Accounts List Rules Maximum 5 accounts stored per device Sorted by lastLoginAt โ€” most recently used first Updated after every successful login (name, avatar may change) If 6th account added โ†’ prompt user to remove one first UI Screens (Dotted) Screen 1 โ€” App Open, One Stored Account โ”Œ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ” [NextGate Logo] โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ [ Avatar ] โ”‚ โ”‚ Joshua Sakweli โ”‚ โ”‚ โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Continue with OTP โ”‚ โ† primary option โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Use Password โ”‚ โ† only if password set โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ G Continue with โ”‚ โ† only if google linked โ”‚ Google โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Not you? Sign in with a different account โ”” โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”˜ Screen 2 โ€” Account Picker (Multiple Stored Accounts) โ”Œ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ” [NextGate Logo] Choose an account โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ [Av] Joshua Sakweli โ†’โ”‚ โ† tap to login โ”‚ โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78 โ”‚ โ”‚ 2 mins ago โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ [Av] QBIT SPARK โ†’โ”‚ โ”‚ โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข32 โ”‚ โ”‚ 3 days ago โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ + Add another accountโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Long press an account to remove it โ”” โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”˜ Screen 3 โ€” Remove Account Confirmation โ”Œ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ” Remove account from this device? โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ [Av] Joshua Sakweliโ”‚ โ”‚ โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ This only removes the account from this device. Your NextGate account will not be deleted. โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Remove โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Cancel โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”” โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”˜ Screen 4 โ€” Fresh Phone Entry (No Stored Account) โ”Œ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ” [NextGate Logo] Enter your phone number to get started โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +255 โ”‚ โ”‚ 7XX XXX XXX โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Continue โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ By continuing you agree to our Terms of Service and Privacy Policy โ”” โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”˜ Screen 5 โ€” OTP Channel Picker โ”Œ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ” Where should we send your code? โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๐Ÿ“ฑ SMS to โ”‚ โ”‚ โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78 โ”‚ โ† tap to choose โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โœ‰๏ธ Email to โ”‚ โ”‚ jโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com โ”‚ โ† tap to choose โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”” โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”˜ Screen 6 โ€” OTP Entry โ”Œ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ” Enter the 6-digit code sent to โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78 โ”Œโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ” โ”‚ 1 โ”‚ โ”‚ 2 โ”‚ โ”‚ 3 โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”˜ Code expires in 01:47 Resend code (available in 0:13) Wrong number? Change it โ”” โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”˜ Screen 7 โ€” Primary Onboarding, Name Step โ”Œ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ” โ— โ—‹ Step 1 of 2 What is your name? โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ First name โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Last name โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Continue โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ This is how you will appear on NextGate โ”” โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”˜ Screen 8 โ€” Primary Onboarding, Age Step โ”Œ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ” โ— โ— Step 2 of 2 When were you born? โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ DD / MM / YYYY โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Continue โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Your age helps us show you the right content. We never share your birthday. โ”” โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”˜ Screen 9 โ€” Secondary Onboarding Gate (Inline, Not Full Screen) โ”Œ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ” โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— โ•‘ Choose a username โ•‘ โ•‘ to create events โ•‘ โ•‘ โ•‘ โ•‘ Step 1 of 2 โ•‘ โ•‘ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ•‘ โ•‘ โ•‘ โ•‘ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ•‘ โ•‘ โ”‚ @username โ”‚ โ•‘ โ•‘ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ•‘ โ•‘ โ•‘ โ•‘ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ•‘ โ•‘ โ”‚ Continue โ”‚ โ•‘ โ•‘ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ•‘ โ•‘ โ•‘ โ•‘ Maybe later โ•‘ โ† dismisses modal โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• user stays on feed โ”” โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”˜ Secondary onboarding appears as a bottom sheet or modal , not a full page. User can dismiss it and continue browsing. They will be prompted again when they try the same action. Screen 10 โ€” Forgot Password โ”Œ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ” Forgot your password? We will send a reset code to your phone number. โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Send reset code โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Login with OTP โ”‚ โ† always available โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Code will be sent to โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78 โ”” โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”˜ Endpoint Reference Public โ€” No Auth Required Method Endpoint Send Receive POST /auth/check { identifier } checkToken + exists + authMethods POST /auth/passwordless/channels { checkToken } available channels masked POST /auth/start { checkToken, channel } tempToken POST /auth/verify { tempToken, otp } onboardingToken or accessToken POST /auth/login/password { checkToken, password } accessToken or device flow POST /auth/login/oauth { checkToken, provider, code } accessToken or onboardingToken POST /auth/resend-otp { tempToken } new tempToken POST /auth/device/verify { deviceVerificationToken, otp } accessToken POST /auth/password/forgot/initiate { checkToken } tempToken POST /auth/password/forgot/verify-otp { tempToken, otp } resetToken POST /auth/password/forgot/reset { resetToken, newPassword, confirmPassword } accessToken Primary Onboarding โ€” Onboarding Token Required Method Endpoint Send Receive POST /auth/onboarding/name { onboardingToken, firstName, lastName } new onboardingToken POST /auth/onboarding/age { onboardingToken, birthDate } accessToken Secondary Onboarding โ€” Access Token Required Method Endpoint Send Receive POST /onboarding/username { username } new accessToken + next action POST /onboarding/bio { bio } new accessToken + next action POST /onboarding/interests { interestIds[] } new accessToken + next action POST /onboarding/profile-pic multipart image new accessToken + next action POST /onboarding/email/initiate { email } tempToken + nextAction POST /onboarding/email/verify { tempToken, otp } new accessToken + next action Session Management โ€” Access Token Required Method Endpoint Send Receive POST /auth/token/refresh { refreshToken } new accessToken + refreshToken POST /auth/token/revoke { refreshToken } success POST /auth/sessions/sign-out โ€” success GET /auth/sessions โ€” active sessions list DELETE /auth/sessions/{id} โ€” success Client-Side Storage Specification Storage Keys ng_stored_accounts โ†’ JSON array of stored account objects ng_active_identifier โ†’ identifier of currently active session Stored Account Object { "identifier": "+255712345678", "maskedPhone": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข78", "displayName": "Joshua Sakweli", "avatarUrl": "https://cdn.nextgate.app/avatars/...", "lastLoginAt": "2026-04-01T10:00:00Z" } Account Management Rules Action What Happens Successful login Add or update entry in stored list. Update lastLoginAt, name, avatar. Normal logout Keep entry in stored list. User sees welcome back on next visit. "Forget this device" logout Remove entry from stored list. Clean phone entry shown next visit. Remove from picker Remove entry from stored list. Account still exists on server. Add another account Login flow, auto-added to list on success. 6th account added Prompt user to remove one existing entry first. Account deleted on server Remove entry from stored list automatically after next failed check. What to Update After Successful Login After ACCESS TOKEN received: โ†’ Update displayName from onboarding flags if changed โ†’ Update avatarUrl if changed โ†’ Update lastLoginAt to now โ†’ Sort stored list by lastLoginAt descending What Changes vs What Stays Being Removed Single linear OnboardingStep tracking โ†’ replaced by independent flags onboardingStep database column โ†’ database migration required isOnboardingComplete() โ†’ replaced by isPrimaryComplete() Onboarding token for secondary steps โ†’ access token handles all of that now refreshOnboardingToken endpoint โ†’ no longer needed Email and username as login identifiers โ†’ phone only from now on Raw identifier passed to /auth/start โ†’ replaced by checkToken + channel Being Added POST /auth/check โ€” new entry point POST /auth/passwordless/channels โ€” new channel check endpoint OnboardingFlagResolver โ€” derives all flags from existing account data Resource guard โ€” checks flags, returns next action automatically checkToken generation in JWT system channel field on /auth/start All secondary onboarding endpoints action and context on all responses Client-side persistent identity (frontend only, zero backend changes) Staying Exactly as They Are All OTP generation, validation, and rate limiting Session creation and management Device trust and registration Risk assessment and scoring Account blocking for underage users and fraud Password change and management Email and phone account linking (post-login) JWT signing infrastructure Security filter chain โ€” minor flag reading addition only Security Notes /auth/check rate limited โ€” max 10 per IP per minute, max 3 per phone per hour checkToken single-use โ€” consumed the moment any auth action is taken checkToken cryptographically binds the phone to every action โ€” identifier cannot be swapped mid-flow Frontend never passes raw email or phone after /auth/check โ€” channel type enum only Orphaned partial accounts cleaned up automatically every night Phone collision with verified account โ€” redirected to login, cannot overwrite Phone collision with unverified account โ€” released and reassigned via OTP proof Primary onboarding โ€” no cancel, no skip, app stays locked until all three steps done Underage โ€” account deleted immediately, phone blocklisted, cannot return until 13th birthday Forgot password link โ€” never shown unless authMethods.password: true Wrong auth method โ€” backend validates before doing anything, 422 returned immediately Stored accounts on device โ€” only display data stored, never tokens or passwords "Forget this device" โ€” clears stored identifier, forces fresh phone entry next visit NextGate PONA Auth โ€” EndPoint Doc (ACTIVE) Author : Josh S. Sakweli, Backend Lead โ€” QBIT SPARK CO LIMITED Last Updated : 2026-04-19 Version : v1.2 Base URL : https://your-api-domain.com/api/v1 For more details on the full flow design : PONA Auth v3 Design Doc What is PONA Auth? P rogressive ยท O nboarding ยท N ative ยท A ccess PONA Auth is NextGate's unified, phone-first authentication system. It replaces all legacy auth flows with a single coherent pipeline. Core philosophy: Phone is the primary identifier โ€” always. Every account starts with a verified phone number. No exceptions. Passwordless by default โ€” users authenticate via OTP. Password and OAuth are optional enhancements added post-registration. Progressive onboarding โ€” only the bare minimum is collected upfront (phone + name + birthdate). Everything else (username, email, bio, interests, profile pic) is collected lazily when the feature needs it. One flow, two outcomes โ€” the same endpoints serve both new and returning users. The server decides what happens based on account state. Token types at a glance Token Expiry Purpose checkToken 10 min Proves a phone check was made. Single-use. tempToken 15 min Carries the OTP session. Single-use after verify. onboardingToken 1 hour Issued after OTP verify for new users. Unlocks primary onboarding. accessToken 1 hour Standard bearer token. Attached to every protected request. refreshToken 30 days Rotates on use. Used to get a new accessToken silently. Secure storage โ€” frontend requirements Store tokens incorrectly and the whole auth system is compromised. Token Where to store Why accessToken In-memory only (React state, Zustand, etc.) Never localStorage โ€” XSS can steal it refreshToken HttpOnly cookie (web) / Secure Keychain (mobile) Never localStorage or AsyncStorage directly onboardingToken In-memory only Short-lived, no need to persist checkToken In-memory only Single-use, discard after consuming tempToken In-memory only Single-use, discard after OTP verify Standard Response Format Success response { "success": true, "httpStatus": "OK", "message": "Human-readable message", "action": "ACTION_CODE", "action_time": "2026-04-03T10:30:45", "data": {} } Error response { "success": false, "httpStatus": "BAD_REQUEST", "message": "Error description", "action_time": "2026-04-03T10:30:45", "data": "Error description" } Action codes Code Meaning Next step REGISTER Phone not found โ€” new user Show registration UI, proceed to channels LOGIN Phone found โ€” existing user Show login UI, proceed to channels CONTINUE_ONBOARDING Phone found but primary incomplete Proceed to channels โ†’ onboarding PROCEED_TO_OTP Only one channel available Skip channel picker, send OTP automatically SELECT_CHANNEL Multiple channels available Show channel picker to user COLLECT_PRIMARY OTP verified, primary data needed Show name + birthdate form ACCOUNT_BLOCKED User is underage or blocked Show blocked message with unblock date VERIFY_DEVICE Unknown device on password login Show device OTP verification OTP Channels OTP can be delivered via the following channels. Not all channels are available in every situation โ€” the server enforces the rules. Available channel values Value Description User selectable SMS OTP delivered via SMS โœ… WHATSAPP OTP delivered via WhatsApp โœ… SMS_AND_WHATSAPP OTP sent to both SMS and WhatsApp simultaneously โœ… EMAIL OTP delivered via email โœ… SMS_AND_WHATSAPP fires both sends in parallel on the server. The user gets the OTP on both channels at the same time. If one channel fails, the other still delivers. EMAIL_AND_WHATSAPP , EMAIL_AND_SMS , ALL_CHANNELS are internal server-side values. Never send these from the client โ€” they will be rejected. Channel rules by purpose Channel New user (registration) Existing user (login) SMS โœ… โœ… WHATSAPP โœ… โœ… SMS_AND_WHATSAPP โœ… โœ… EMAIL โŒ not allowed โœ… only if account has a verified email Channel request examples Send via SMS only: { "channel": "SMS" } Send via WhatsApp only: { "channel": "WHATSAPP" } Send via both SMS and WhatsApp at the same time: { "channel": "SMS_AND_WHATSAPP" } Send via email (login only, verified email required): { "channel": "EMAIL" } Shared Objects UserInfo Returned by /auth/verify-otp and /auth/onboarding/primary once the user is identified. Frontend devs should persist this in local storage for display use (profile header, greetings, etc.). { "displayName": "Joshua Sakweli", "phone": "+255745051250", "maskedPhone": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "avatarUrl": "https://cdn.example.com/avatars/uuid.jpg" } Field Type Description displayName string | null First + last name. Null until primary onboarding is complete. phone string Full unmasked phone in international format. Always present. Safe to store โ€” it is the user's own number, just verified via OTP. maskedPhone string Masked phone for visible UI display (e.g. "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50"). Always present. avatarUrl string | null URL of the user's profile picture. Null until a profile picture is uploaded. Storage guidance : phone , maskedPhone , displayName , and avatarUrl are display data โ€” localStorage is fine. Do not store tokens in localStorage. HTTP Method Badges GET โ€” Read only POST โ€” Create / action DELETE โ€” Remove Endpoints 1. Check Phone Purpose : Entry point for every auth flow. Checks if a phone number is registered and returns a checkToken plus available auth methods. Endpoint : POST {base_url}/auth/check Access Level : ๐ŸŒ Public Authentication : None Request : { "identifier": "+255745051250", "deviceId": "android-uuid-abc123" } Request Parameters : Parameter Type Required Description Validation identifier string Yes Phone number in international format Must match ^\+[1-9]\d{6,14}$ deviceId string Yes Unique device identifier from the client Non-empty Response โ€” New User : { "success": true, "httpStatus": "OK", "message": "Phone number not registered", "action": "REGISTER", "action_time": "2026-04-03T10:30:45", "data": { "exists": false, "checkToken": "eyJhbGciOiJIUzI1NiJ9...", "primaryComplete": false, "maskedPhone": null, "authMethods": null } } Response โ€” Existing User : { "success": true, "httpStatus": "OK", "message": "Welcome back", "action": "LOGIN", "action_time": "2026-04-03T10:30:45", "data": { "exists": true, "checkToken": "eyJhbGciOiJIUzI1NiJ9...", "primaryComplete": true, "maskedPhone": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "authMethods": { "passwordless": true, "password": false, "google": true, "apple": false } } } Response โ€” Existing User, Primary Incomplete : { "success": true, "httpStatus": "OK", "message": "Continue setting up your account", "action": "CONTINUE_ONBOARDING", "action_time": "2026-04-03T10:30:45", "data": { "exists": true, "checkToken": "eyJhbGciOiJIUzI1NiJ9...", "primaryComplete": false, "maskedPhone": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "authMethods": { "passwordless": true, "password": false, "google": false, "apple": false } } } Response Fields : Field Description exists Whether the phone is registered checkToken Short-lived token to proceed. Always present. primaryComplete Whether the user has completed name + birthdate setup maskedPhone Masked phone for display. Null for new users. authMethods.passwordless Always true authMethods.password True if user has set a password authMethods.google True if Google is linked authMethods.apple True if Apple is linked Frontend handling : action = REGISTER โ†’ store checkToken in memory โ†’ do NOT show password field โ†’ do NOT show Google/Apple buttons โ†’ proceed to channel picker action = LOGIN โ†’ store checkToken in memory โ†’ show Google button ONLY if authMethods.google = true โ†’ show Password button ONLY if authMethods.password = true โ†’ always show OTP button โ†’ proceed to channel picker action = CONTINUE_ONBOARDING โ†’ same as LOGIN โ†’ user will be redirected to primary onboarding after OTP verify Errors : 422 UNPROCESSABLE_ENTITY โ€” invalid phone format or missing deviceId 2. Get Passwordless Channels Purpose : Returns the available OTP delivery channels for the user. Always returns SMS and WHATSAPP. EMAIL is returned only if the user has a verified email. Endpoint : POST {base_url}/auth/passwordless/channels Access Level : ๐ŸŒ Public Authentication : None This endpoint does NOT consume the checkToken. Request : { "checkToken": "eyJhbGciOiJIUzI1NiJ9...", "deviceId": "android-uuid-abc123" } Request Parameters : Parameter Type Required Description checkToken string Yes Token from /auth/check deviceId string Yes Must match the deviceId used in /auth/check Response โ€” Phone only : { "success": true, "httpStatus": "OK", "message": "Choose where to receive your code", "action": "SELECT_CHANNEL", "action_time": "2026-04-16T10:30:45", "data": { "channels": [ { "channel": "SMS", "masked": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "isPrimary": true }, { "channel": "WHATSAPP", "masked": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "isPrimary": false } ] } } Response โ€” Phone + verified email : { "success": true, "httpStatus": "OK", "message": "Choose where to receive your code", "action": "SELECT_CHANNEL", "action_time": "2026-04-16T10:30:45", "data": { "channels": [ { "channel": "SMS", "masked": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "isPrimary": true }, { "channel": "WHATSAPP", "masked": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "isPrimary": false }, { "channel": "EMAIL", "masked": "jโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com", "isPrimary": false } ] } } This endpoint returns individual primitive channels only ( SMS , WHATSAPP , EMAIL ). The SMS_AND_WHATSAPP compound value is not returned here โ€” the frontend constructs it when the user wants both. Frontend handling : Always show at least SMS and WHATSAPP. If EMAIL is present, show it too. Suggested UI: SMS โ†’ "Text message to โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50" WHATSAPP โ†’ "WhatsApp to โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50" EMAIL โ†’ "Email to jโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com" You can also show a "Send to both SMS and WhatsApp" option โ€” send SMS_AND_WHATSAPP as the channel value in /auth/passwordless-start. User taps their choice, then call /auth/passwordless-start with that channel value. Errors : 403 FORBIDDEN โ€” invalid, expired, or already-used checkToken 403 FORBIDDEN โ€” deviceId mismatch 3. Start Passwordless OTP Purpose : Sends an OTP to the chosen channel and returns a tempToken for the verify step. Consumes the checkToken . Endpoint : POST {base_url}/auth/passwordless-start Access Level : ๐ŸŒ Public Authentication : None Request โ€” SMS only : { "checkToken": "eyJhbGciOiJIUzI1NiJ9...", "channel": "SMS", "deviceId": "android-uuid-abc123" } Request โ€” WhatsApp only : { "checkToken": "eyJhbGciOiJIUzI1NiJ9...", "channel": "WHATSAPP", "deviceId": "android-uuid-abc123" } Request โ€” Both SMS and WhatsApp simultaneously : { "checkToken": "eyJhbGciOiJIUzI1NiJ9...", "channel": "SMS_AND_WHATSAPP", "deviceId": "android-uuid-abc123" } Request โ€” Email (login only, verified email required) : { "checkToken": "eyJhbGciOiJIUzI1NiJ9...", "channel": "EMAIL", "deviceId": "android-uuid-abc123" } Request Parameters : Parameter Type Required Description Validation checkToken string Yes Token from /auth/check Non-empty channel enum Yes Where to send OTP SMS , WHATSAPP , SMS_AND_WHATSAPP , EMAIL deviceId string Yes Must match deviceId from /auth/check Non-empty Channel rules : Channel New user (registration) Existing user (login) SMS โœ… โœ… WHATSAPP โœ… โœ… SMS_AND_WHATSAPP โœ… โœ… EMAIL โŒ โœ… only if verified email exists Response : { "success": true, "httpStatus": "OK", "message": "Verification code sent", "action_time": "2026-04-16T10:30:45", "data": { "tempToken": "eyJhbGciOiJIUzI1NiJ9...", "maskedDestination": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "channel": "SMS_AND_WHATSAPP", "expiresInSeconds": 120, "resendAvailableAfterSeconds": 60 } } Response Fields : Field Description tempToken Carry this to /auth/verify-otp . Store in memory only. maskedDestination Show to the user so they know where OTP was sent channel The channel used โ€” display appropriate message expiresInSeconds OTP valid for this many seconds resendAvailableAfterSeconds Wait this long before enabling resend Frontend handling : On success: โ†’ store tempToken in memory โ†’ show OTP input screen โ†’ display message based on channel: SMS โ†’ "Code sent to โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50 via SMS" WHATSAPP โ†’ "Code sent to โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50 via WhatsApp" SMS_AND_WHATSAPP โ†’ "Code sent to โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50 via SMS and WhatsApp" EMAIL โ†’ "Code sent to jโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข@gโ€ขโ€ขโ€ขโ€ข.com" โ†’ start countdown timer using resendAvailableAfterSeconds โ†’ enable resend button when timer hits 0 On resend: โ†’ channel is locked to the original choice โ†’ resend always goes to the same channel(s) โ†’ to switch channel, go back to the channel picker and restart the flow Errors : 403 FORBIDDEN โ€” checkToken invalid, expired, or already consumed 400 BAD_REQUEST โ€” EMAIL chosen but account has no verified email 400 BAD_REQUEST โ€” EMAIL chosen for registration 400 BAD_REQUEST โ€” non-user-selectable channel sent (e.g. ALL_CHANNELS) 422 UNPROCESSABLE_ENTITY โ€” invalid channel value 4. Verify OTP Purpose : Validates the OTP and returns either an accessToken (returning user, primary complete) or an onboardingToken (new or incomplete user). Endpoint : POST {base_url}/auth/verify-otp Access Level : ๐ŸŒ Public Authentication : None Request : { "tempToken": "eyJhbGciOiJIUzI1NiJ9...", "otp": "482910", "deviceName": "Josh's Pixel 4a", "platform": "ANDROID" } Request Parameters : Parameter Type Required Description Validation tempToken string Yes Token from /auth/passwordless-start Non-empty otp string Yes 6-digit code Exactly 6 numeric digits deviceName string No Human-readable device name Optional platform string No Client platform ANDROID , IOS , WEB Response โ€” Primary Complete : { "success": true, "httpStatus": "OK", "message": "Welcome back", "action": null, "action_time": "2026-04-03T10:30:45", "data": { "accessToken": "eyJhbGciOiJIUzI1NiJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiJ9...", "onboardingToken": null, "primaryComplete": true, "onboarding": { "primaryComplete": true, "username": true, "email": false, "profilePic": false, "interests": false, "bio": false }, "user": { "displayName": "Joshua Sakweli", "phone": "+255745051250", "maskedPhone": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "avatarUrl": null } } } Response โ€” Primary Incomplete : { "success": true, "httpStatus": "OK", "message": "Phone verified. Let us set up your account.", "action": "COLLECT_PRIMARY", "action_time": "2026-04-03T10:30:45", "data": { "accessToken": null, "refreshToken": null, "onboardingToken": "eyJhbGciOiJIUzI1NiJ9...", "primaryComplete": false, "onboarding": { "primaryComplete": false, "username": false, "email": false, "profilePic": false, "interests": false, "bio": false }, "user": { "displayName": null, "phone": "+255745051250", "maskedPhone": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "avatarUrl": null } } } Frontend handling : primaryComplete = true: โ†’ store accessToken in memory โ†’ store refreshToken in HttpOnly cookie (web) or Keychain (mobile) โ†’ discard tempToken โ†’ navigate to home โ†’ check onboarding flags for secondary prompts primaryComplete = false: โ†’ store onboardingToken in memory โ†’ discard tempToken โ†’ navigate to primary onboarding screen Errors : 403 FORBIDDEN โ€” wrong OTP 403 FORBIDDEN โ€” OTP expired 403 FORBIDDEN โ€” max attempts exceeded (3 wrong OTPs) 403 FORBIDDEN โ€” tempToken already used 5. Resend OTP Purpose : Resends the OTP to the same channel and destination as the original send. Channel cannot be changed on resend. Rate limited to 5 attempts per session with a 60-second cooldown. Endpoint : POST {base_url}/auth/resend-otp Access Level : ๐ŸŒ Public Authentication : None Request : { "tempToken": "eyJhbGciOiJIUzI1NiJ9..." } Response : { "success": true, "httpStatus": "OK", "message": "OTP resent successfully", "action_time": "2026-04-03T10:30:45", "data": { "tempToken": "eyJhbGciOiJIUzI1NiJ9...", "maskedIdentifier": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "remainingAttempts": 4, "expiresIn": 900 } } Frontend handling : On success: โ†’ replace tempToken in memory with the new one from response โ†’ show "Code resent" confirmation โ†’ reset the countdown timer to resendAvailableAfterSeconds โ†’ disable resend button again On 400 โ€” cooldown active: โ†’ show "Please wait X seconds" โ†’ do not clear the OTP input On 400 โ€” max attempts: โ†’ show "Too many attempts. Please start over." โ†’ clear tempToken from memory โ†’ navigate back to channel picker Channel switching: โ†’ NOT possible via resend โ†’ user must go back to channel picker and call /auth/passwordless-start again Errors : 400 BAD_REQUEST โ€” cooldown period not yet elapsed 400 BAD_REQUEST โ€” max resend attempts (5) reached 400 BAD_REQUEST โ€” tempToken expired or invalid 6. Primary Onboarding Purpose : Collects name and date of birth. Completes primary onboarding and issues the first accessToken . Endpoint : POST {base_url}/auth/onboarding/primary Access Level : ๐ŸŒ Public Authentication : None (uses onboardingToken in body) Request : { "onboardingToken": "eyJhbGciOiJIUzI1NiJ9...", "firstName": "Joshua", "lastName": "Sakweli", "birthDate": "1995-06-15" } Request Parameters : Parameter Type Required Description Validation onboardingToken string Yes Token from /auth/verify-otp Non-empty firstName string Yes User's first name 1โ€“50 characters lastName string Yes User's last name 1โ€“50 characters birthDate string Yes Date of birth YYYY-MM-DD , must be in the past Response โ€” Normal User : { "success": true, "httpStatus": "OK", "message": "Welcome to NextGate!", "action_time": "2026-04-03T10:30:45", "data": { "accessToken": "eyJhbGciOiJIUzI1NiJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiJ9...", "accountTier": "FULL", "onboarding": { "primaryComplete": true, "username": false, "email": false, "profilePic": false, "interests": false, "bio": false }, "blocked": false, "unblockDate": null, "user": { "displayName": "Joshua Sakweli", "phone": "+255745051250", "maskedPhone": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "avatarUrl": null } } } Response โ€” Underage User : { "success": true, "httpStatus": "OK", "message": "Account blocked", "action": "ACCOUNT_BLOCKED", "action_time": "2026-04-03T10:30:45", "data": { "accessToken": null, "refreshToken": null, "accountTier": null, "onboarding": null, "blocked": true, "unblockDate": "2026-09-15" } } Response Fields : Field Description accessToken Bearer token. Store in memory. Null if blocked. refreshToken Rotation token. Store securely. Null if blocked. accountTier FULL (18+), RESTRICTED (13โ€“17), MINOR (under 13 โ€” blocked) blocked True if user is underage unblockDate Date when user turns 13. Show to user. Frontend handling : blocked = false: โ†’ store accessToken in memory โ†’ store refreshToken securely โ†’ discard onboardingToken โ†’ navigate to home โ†’ check onboarding flags for secondary prompts blocked = true: โ†’ do NOT store any tokens โ†’ show age restriction screen with unblockDate โ†’ do NOT allow navigation into the app accountTier = RESTRICTED: โ†’ user is 13โ€“17 โ†’ restrict features per your tier config Errors : 403 FORBIDDEN โ€” onboardingToken invalid or expired 403 FORBIDDEN โ€” primary onboarding already completed 422 UNPROCESSABLE_ENTITY โ€” validation errors on name or birthDate 7. Password Login Purpose : Authenticates a user with phone + password. May require device verification if the device is unknown or risk is high. Endpoint : POST {base_url}/auth/login/password Access Level : ๐ŸŒ Public Authentication : None Request : { "checkToken": "eyJhbGciOiJIUzI1NiJ9...", "password": "MySecurePassword123", "deviceId": "android-uuid-abc123", "deviceName": "Josh's Pixel 4a", "platform": "ANDROID" } Request Parameters : Parameter Type Required Description Validation checkToken string Yes Token from /auth/check Non-empty password string Yes User's password Non-empty deviceId string Yes Device identifier Non-empty deviceName string No Human-readable device name Optional platform string No Client platform ANDROID , IOS , WEB Response โ€” Known Device : { "success": true, "httpStatus": "OK", "message": "Login successful", "action_time": "2026-04-03T10:30:45", "data": { "accessToken": "eyJhbGciOiJIUzI1NiJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiJ9...", "onboarding": { "primaryComplete": true, "username": true, "email": false, "profilePic": false, "interests": true, "bio": false }, "requiresDeviceVerification": false, "deviceVerificationToken": null, "maskedDestination": null } } Response โ€” Unknown / High Risk Device : { "success": true, "httpStatus": "OK", "message": "Device verification required", "action": "VERIFY_DEVICE", "action_time": "2026-04-03T10:30:45", "data": { "accessToken": null, "refreshToken": null, "requiresDeviceVerification": true, "deviceVerificationToken": "eyJhbGciOiJIUzI1NiJ9...", "maskedDestination": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50" } } Frontend handling : requiresDeviceVerification = false: โ†’ store accessToken in memory โ†’ store refreshToken securely โ†’ navigate to home requiresDeviceVerification = true: โ†’ store deviceVerificationToken in memory โ†’ show OTP input with maskedDestination โ†’ call POST /api/v1/account/device/verify with the OTP โ†’ on success you get accessToken + refreshToken Errors : 403 FORBIDDEN โ€” wrong password 403 FORBIDDEN โ€” checkToken invalid or expired 403 FORBIDDEN โ€” too many failed attempts 403 FORBIDDEN โ€” password not set on this account 8. OAuth Login Purpose : Authenticates a user via Google or Apple. Only available if the provider was previously linked to the account. Endpoint : POST {base_url}/auth/login/oauth Access Level : ๐ŸŒ Public Authentication : None Request : { "checkToken": "eyJhbGciOiJIUzI1NiJ9...", "provider": "GOOGLE", "idToken": "google-id-token-from-client-sdk", "deviceId": "android-uuid-abc123", "deviceName": "Josh's Pixel 4a", "platform": "ANDROID", "state": "optional-state-string" } Request Parameters : Parameter Type Required Description Validation checkToken string Yes Token from /auth/check Non-empty provider string Yes OAuth provider GOOGLE , APPLE idToken string Yes ID token from Google/Apple client SDK Non-empty deviceId string Yes Device identifier Non-empty deviceName string No Human-readable device name Optional platform string No Client platform ANDROID , IOS , WEB state string No Opaque state value passed back in response Optional Response : { "success": true, "httpStatus": "OK", "message": "Login successful", "action_time": "2026-04-03T10:30:45", "data": { "accessToken": "eyJhbGciOiJIUzI1NiJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiJ9...", "onboarding": { "primaryComplete": true, "username": true, "email": true, "profilePic": false, "interests": true, "bio": false }, "state": "optional-state-string" } } Frontend handling : Before calling this endpoint: โ†’ check authMethods.google from /auth/check response โ†’ ONLY show Google button if google = true โ†’ ONLY show Apple button if apple = true On success: โ†’ store accessToken in memory โ†’ store refreshToken securely โ†’ navigate to home Errors : 403 FORBIDDEN โ€” provider not linked ( OAUTH_NOT_LINKED ) 403 FORBIDDEN โ€” idToken invalid or expired 403 FORBIDDEN โ€” idToken email does not match linked provider 403 FORBIDDEN โ€” checkToken invalid or expired 9. Forgot Password โ€” Initiate Purpose : Starts the forgot password flow. Sends an OTP to the user's phone via SMS. Does NOT consume the checkToken. Endpoint : POST {base_url}/auth/password/forgot/initiate Access Level : ๐ŸŒ Public Authentication : None Request : { "checkToken": "eyJhbGciOiJIUzI1NiJ9...", "deviceId": "android-uuid-abc123" } Response : { "success": true, "httpStatus": "OK", "message": "Password reset code sent to your phone", "action_time": "2026-04-03T10:30:45", "data": { "tempToken": "eyJhbGciOiJIUzI1NiJ9...", "resetToken": null, "maskedPhone": "โ€ขโ€ขโ€ข โ€ขโ€ขโ€ข โ€ขโ€ข50", "accessToken": null, "expiresInSeconds": 120 } } Frontend handling : โ†’ store tempToken in memory โ†’ show OTP input screen โ†’ display maskedPhone โ†’ proceed to /auth/password/forgot/verify-otp Errors : 403 FORBIDDEN โ€” checkToken invalid or expired 403 FORBIDDEN โ€” account has no password set 404 NOT_FOUND โ€” account not found 10. Forgot Password โ€” Verify OTP Purpose : Verifies the OTP and issues a short-lived resetToken . Endpoint : POST {base_url}/auth/password/forgot/verify-otp Access Level : ๐ŸŒ Public Authentication : None Request : { "tempToken": "eyJhbGciOiJIUzI1NiJ9...", "otp": "482910" } Response : { "success": true, "httpStatus": "OK", "message": "Identity confirmed. Set your new password.", "action_time": "2026-04-03T10:30:45", "data": { "tempToken": null, "resetToken": "eyJhbGciOiJIUzI1NiJ9...", "maskedPhone": null, "accessToken": null, "expiresInSeconds": 0 } } Frontend handling : โ†’ discard tempToken from memory โ†’ store resetToken in memory โ†’ navigate to new password input screen Errors : 403 FORBIDDEN โ€” wrong OTP 403 FORBIDDEN โ€” OTP expired 403 FORBIDDEN โ€” max OTP attempts exceeded 11. Forgot Password โ€” Reset Purpose : Sets the new password. Revokes all existing sessions and issues a fresh accessToken . Endpoint : POST {base_url}/auth/password/forgot/reset Access Level : ๐ŸŒ Public Authentication : None Request : { "resetToken": "eyJhbGciOiJIUzI1NiJ9...", "newPassword": "MyNewSecurePassword456", "confirmPassword": "MyNewSecurePassword456" } Request Parameters : Parameter Type Required Description Validation resetToken string Yes Token from verify OTP step Non-empty newPassword string Yes New password Min 8 characters confirmPassword string Yes Must match newPassword Non-empty Response : { "success": true, "httpStatus": "OK", "message": "Password updated. All other sessions signed out.", "action_time": "2026-04-03T10:30:45", "data": { "accessToken": "eyJhbGciOiJIUzI1NiJ9...", "resetToken": null, "maskedPhone": null, "expiresInSeconds": 0 } } Frontend handling : โ†’ discard resetToken from memory โ†’ store accessToken in memory โ†’ clear any existing refreshToken from storage โ†’ navigate to home โ†’ show "Password updated successfully" Errors : 400 BAD_REQUEST โ€” passwords do not match 403 FORBIDDEN โ€” resetToken invalid or expired 422 UNPROCESSABLE_ENTITY โ€” password too short 12. Refresh Token Purpose : Exchanges a refresh token for a new access + refresh token pair. Old refresh token is invalidated immediately (rotation). Endpoint : POST {base_url}/auth/token/refresh Access Level : ๐ŸŒ Public Authentication : None Request : { "refreshToken": "eyJhbGciOiJIUzI1NiJ9..." } Response : { "success": true, "httpStatus": "OK", "message": "Token refreshed", "action_time": "2026-04-03T10:30:45", "data": { "accessToken": "eyJhbGciOiJIUzI1NiJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiJ9...", "expiresIn": 3600 } } Frontend handling : Call this when: โ†’ accessToken is expired (401 on a protected request) โ†’ proactively before expiry (check exp claim in JWT) On success: โ†’ replace accessToken in memory โ†’ replace refreshToken in secure storage โ†’ retry the original failed request On 401: โ†’ clear all tokens โ†’ redirect to login Errors : 401 UNAUTHORIZED โ€” refresh token invalid, expired, or revoked 401 UNAUTHORIZED โ€” token reuse detected โ€” session revoked 13. Revoke Token Purpose : Logs out the user by revoking their refresh token. Endpoint : POST {base_url}/auth/token/revoke Access Level : ๐ŸŒ Public Authentication : None Request : { "refreshToken": "eyJhbGciOiJIUzI1NiJ9..." } Response : { "success": true, "httpStatus": "OK", "message": "Token revoked successfully", "action_time": "2026-04-03T10:30:45", "data": null } Frontend handling : On logout: โ†’ call this endpoint with the stored refreshToken โ†’ clear accessToken from memory โ†’ clear refreshToken from secure storage โ†’ redirect to login If call fails (network error): โ†’ still clear tokens locally โ†’ user is effectively logged out on the client Quick Reference โ€” Full Auth Flow 1. POST /auth/check โ†’ phone + deviceId โ†’ checkToken + action 2. POST /auth/passwordless/channels (does not consume checkToken) โ†’ returns available channels: SMS, WHATSAPP, and optionally EMAIL 3. POST /auth/passwordless-start (consumes checkToken) โ†’ channel (SMS | WHATSAPP | SMS_AND_WHATSAPP | EMAIL) โ†’ tempToken + OTP sent 4. POST /auth/verify-otp (consumes tempToken) โ†’ otp โ†’ accessToken (returning user) or onboardingToken (new user) 5. POST /auth/onboarding/primary (if onboardingToken received) โ†’ name + birthDate โ†’ accessToken issued โ”€โ”€โ”€ User is now logged in โ”€โ”€โ”€ 6. Secondary onboarding (optional, progressive) โ†’ username, email, interests, bio, profile pic โ†’ each step returns new accessToken with updated onboarding flags โ”€โ”€โ”€ Token management โ”€โ”€โ”€ 7. POST /auth/token/refresh โ†’ rotate tokens silently 8. POST /auth/token/revoke โ†’ logout Error Handling Summary HTTP Status When it happens What to do 400 BAD_REQUEST Invalid input, item exists, rate limit Show error message to user 401 UNAUTHORIZED Token expired or invalid Refresh token or redirect to login 403 FORBIDDEN Wrong OTP, wrong password, token mismatch Show specific error, let user retry 404 NOT_FOUND Account not found Show "Account not found" 422 UNPROCESSABLE_ENTITY Validation failed Show field-level errors 500 INTERNAL_SERVER_ERROR Server error Show generic error, retry PONA Auth Secondary Onboarding Author : Josh S. Sakweli, Backend Lead Team Last Updated : 2026-04-27 Version : v1.0 Base URL : https://api.nexgate.co/api/v1/onboarding/secondary Short Description : Secondary onboarding completes a user's profile after the primary onboarding step (name, phone, birth date). It is a step-machine โ€” each endpoint returns the next missing step and a refreshed access token. The flow is complete when stepsRemaining reaches 0 and nextMissing is null . Hints : All endpoints require a valid Bearer access token issued after primary onboarding or login. Every response includes a fresh accessToken with updated onboarding flags embedded in the JWT claims โ€” replace the stored token after each step. Steps can be completed in any order. The nextMissing field signals the next recommended step; it does not enforce ordering. Email linking has two independent paths: custom (user-provided email + OTP) and Google (Google ID token). Only one path needs to be completed. Profile picture upload expects multipart/form-data , not JSON. Standard Response Format All API responses follow a consistent structure using our Globe Response Builder pattern: Success Response Structure { "success": true, "httpStatus": "OK", "message": "Operation completed successfully", "action_time": "2025-09-23T10:30:45", "action": "COLLECT_EMAIL", "data": {} } Error Response Structure { "success": false, "httpStatus": "BAD_REQUEST", "message": "Error description", "action_time": "2025-09-23T10:30:45", "data": "Error description" } Standard Response Fields Field Type Description success boolean true for successful operations, false for errors httpStatus string HTTP status name (OK, BAD_REQUEST, etc.) message string Human-readable result description action_time string ISO 8601 timestamp of the response action string Next frontend action to perform (see action codes below) data object/string Response payload or error detail Action Codes Code Meaning COLLECT_USERNAME Username step is next COLLECT_EMAIL Email step is next COLLECT_PROFILE_PIC Profile picture step is next COLLECT_INTERESTS Interests step is next COLLECT_BIO Bio step is next PROCEED All steps complete โ€” onboarding is done Standard Secondary Onboarding Response Fields Most endpoints return a SecondaryOnboardingResponse . Its fields are: Field Type Description accessToken string Fresh JWT โ€” store and use this for all subsequent requests onboarding object Current completion state of all onboarding steps (see below) nextMissing string Key name of the next incomplete step, or null if all done stepsRemaining integer Number of steps still pending onboarding Object Fields Field Type Description primaryComplete boolean Primary onboarding (name/phone/DOB) is done username boolean Username has been set email boolean Email has been linked and verified profilePic boolean Profile picture has been uploaded interests boolean Interests have been selected bio boolean Bio has been written Endpoints 1. Get Username Suggestions Purpose : Returns up to 5 AI-generated username suggestions based on the user's first name, last name, and birth date. Endpoint : GET {base_url}/username/suggestions Access Level : ๐Ÿ”’ Protected (Authenticated user) Authentication : Bearer Token Request Headers : Header Type Required Description Authorization string Yes Bearer Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Username suggestions", "action_time": "2026-04-27T10:30:45", "data": { "suggestions": [ "john_sakweli", "johnsakweli99", "j_sakweli", "johnsak2004", "jsakweli_" ] } } Success Response Fields : Field Description suggestions Array of up to 5 available username strings Error Response JSON Sample : { "success": false, "httpStatus": "NOT_FOUND", "message": "Account not found", "action_time": "2026-04-27T10:30:45", "data": "Account not found" } 2. Set Username Purpose : Sets a unique username for the account. Endpoint : POST {base_url}/username Access Level : ๐Ÿ”’ Protected (Authenticated user) Authentication : Bearer Token Request Headers : Header Type Required Description Authorization string Yes Bearer Content-Type string Yes application/json Request JSON Sample : { "username": "john_sakweli" } Request Body Parameters : Parameter Type Required Description Validation username string Yes Desired username Min: 3, Max: 30 characters. Must start with a letter. Only letters, numbers, and underscores allowed. Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Username set successfully", "action_time": "2026-04-27T10:30:45", "action": "COLLECT_EMAIL", "data": { "accessToken": "eyJhbGciOiJSUzI1NiJ9...", "onboarding": { "primaryComplete": true, "username": true, "email": false, "profilePic": false, "interests": false, "bio": false }, "nextMissing": "email", "stepsRemaining": 4 } } Success Response Fields : Field Description accessToken Fresh JWT with updated onboarding claims โ€” replace stored token onboarding.username Now true nextMissing Next recommended step key stepsRemaining Steps left to complete Error Response JSON Sample : { "success": false, "httpStatus": "BAD_REQUEST", "message": "Username is already taken", "action_time": "2026-04-27T10:30:45", "data": "Username is already taken" } Standard Error Types : 400 BAD_REQUEST : Username already taken or invalid format 401 UNAUTHORIZED : Missing or invalid token 422 UNPROCESSABLE_ENTITY : Validation failure (length, pattern) 3. Set Bio Purpose : Saves a short bio to the user's profile. Endpoint : POST {base_url}/bio Access Level : ๐Ÿ”’ Protected (Authenticated user) Authentication : Bearer Token Request Headers : Header Type Required Description Authorization string Yes Bearer Content-Type string Yes application/json Request JSON Sample : { "bio": "Event enthusiast, live music lover, always at the front row." } Request Body Parameters : Parameter Type Required Description Validation bio string Yes User's short profile bio Max: 160 characters. Cannot be blank. Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Bio saved", "action_time": "2026-04-27T10:30:45", "action": "PROCEED", "data": { "accessToken": "eyJhbGciOiJSUzI1NiJ9...", "onboarding": { "primaryComplete": true, "username": true, "email": true, "profilePic": true, "interests": true, "bio": true }, "nextMissing": null, "stepsRemaining": 0 } } Standard Error Types : 400 BAD_REQUEST : Bio is blank 401 UNAUTHORIZED : Missing or invalid token 422 UNPROCESSABLE_ENTITY : Exceeds 160 characters 4. Set Interests Purpose : Saves the user's selected interest categories (minimum 3 required). Endpoint : POST {base_url}/interests Access Level : ๐Ÿ”’ Protected (Authenticated user) Authentication : Bearer Token Request Headers : Header Type Required Description Authorization string Yes Bearer Content-Type string Yes application/json Request JSON Sample : { "interestIds": [ "3fa85f64-5717-4562-b3fc-2c963f66afa6", "7a1234bc-1234-4321-abcd-1234567890ab", "9c87654d-4321-1234-dcba-0987654321cd" ] } Request Body Parameters : Parameter Type Required Description Validation interestIds array of UUID Yes IDs of selected interest categories Min: 3 items. Must not be empty. Use the interests listing endpoint to get valid IDs. Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Interests saved", "action_time": "2026-04-27T10:30:45", "action": "COLLECT_BIO", "data": { "accessToken": "eyJhbGciOiJSUzI1NiJ9...", "onboarding": { "primaryComplete": true, "username": true, "email": true, "profilePic": true, "interests": true, "bio": false }, "nextMissing": "bio", "stepsRemaining": 1 } } Standard Error Types : 400 BAD_REQUEST : Interest IDs not found 401 UNAUTHORIZED : Missing or invalid token 422 UNPROCESSABLE_ENTITY : Fewer than 3 interests selected 5. Upload Profile Picture Purpose : Uploads and stores the user's profile picture. Endpoint : POST {base_url}/profile-pic Access Level : ๐Ÿ”’ Protected (Authenticated user) Authentication : Bearer Token Request Headers : Header Type Required Description Authorization string Yes Bearer Content-Type string Yes multipart/form-data Query Parameters : Parameter Type Required Description Validation Default file file (form field) Yes Image file to upload Image formats: JPEG, PNG, WEBP โ€” Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Profile picture uploaded", "action_time": "2026-04-27T10:30:45", "action": "COLLECT_INTERESTS", "data": { "accessToken": "eyJhbGciOiJSUzI1NiJ9...", "onboarding": { "primaryComplete": true, "username": true, "email": true, "profilePic": true, "interests": false, "bio": false }, "nextMissing": "interests", "stepsRemaining": 2 } } Standard Error Types : 400 BAD_REQUEST : File is missing, empty, or unsupported format 401 UNAUTHORIZED : Missing or invalid token 500 INTERNAL_SERVER_ERROR : Storage failure 6. Initiate Email Linking (Custom) Purpose : Sends a 6-digit OTP to the provided email address and returns a tempToken required for the verify step. Endpoint : POST {base_url}/email/custom/initiate Access Level : ๐Ÿ”’ Protected (Authenticated user) Authentication : Bearer Token Request Headers : Header Type Required Description Authorization string Yes Bearer Content-Type string Yes application/json Request JSON Sample : { "email": "john@example.com" } Request Body Parameters : Parameter Type Required Description Validation email string Yes Email address to link Must be a valid email format. Cannot be blank. Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Verification code sent to your email", "action_time": "2026-04-27T10:30:45", "data": { "tempToken": "eyJhbGciOiJSUzI1NiJ9.temp...", "nextAction": "VERIFY_EMAIL" } } Success Response Fields : Field Description tempToken Short-lived token required as input to the verify step. Store this until OTP is submitted. nextAction Always VERIFY_EMAIL โ€” signals frontend to show OTP input screen Standard Error Types : 400 BAD_REQUEST : Email already in use by another account 401 UNAUTHORIZED : Missing or invalid token 422 UNPROCESSABLE_ENTITY : Invalid email format 7. Verify Email (Custom) Purpose : Confirms the OTP sent to the user's email and marks the email step as complete. Endpoint : POST {base_url}/email/custom/verify Access Level : ๐Ÿ”’ Protected (Authenticated user) Authentication : Bearer Token Request Headers : Header Type Required Description Authorization string Yes Bearer Content-Type string Yes application/json Request JSON Sample : { "tempToken": "eyJhbGciOiJSUzI1NiJ9.temp...", "otp": "847291" } Request Body Parameters : Parameter Type Required Description Validation tempToken string Yes Token received from the initiate step Cannot be blank otp string Yes 6-digit code sent to the user's email Exactly 6 digits, numeric only Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Email verified", "action_time": "2026-04-27T10:30:45", "action": "COLLECT_PROFILE_PIC", "data": { "accessToken": "eyJhbGciOiJSUzI1NiJ9...", "onboarding": { "primaryComplete": true, "username": true, "email": true, "profilePic": false, "interests": false, "bio": false }, "nextMissing": "profilePic", "stepsRemaining": 3 } } Standard Error Types : 400 BAD_REQUEST : OTP is incorrect or expired 401 UNAUTHORIZED : tempToken is invalid or expired 422 UNPROCESSABLE_ENTITY : OTP is not 6 numeric digits 8. Link Email via Google Purpose : Links and verifies a Google-backed email using a Google ID token โ€” skips the OTP step entirely. Endpoint : POST {base_url}/email/google Access Level : ๐Ÿ”’ Protected (Authenticated user) Authentication : Bearer Token Request Headers : Header Type Required Description Authorization string Yes Bearer Content-Type string Yes application/json Request JSON Sample : { "idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6..." } Request Body Parameters : Parameter Type Required Description Validation idToken string Yes Google ID token from the Google Sign-In SDK Cannot be blank. Must be a valid Google-issued token. Success Response JSON Sample : { "success": true, "httpStatus": "OK", "message": "Email linked via Google", "action_time": "2026-04-27T10:30:45", "action": "COLLECT_PROFILE_PIC", "data": { "accessToken": "eyJhbGciOiJSUzI1NiJ9...", "onboarding": { "primaryComplete": true, "username": true, "email": true, "profilePic": false, "interests": false, "bio": false }, "nextMissing": "profilePic", "stepsRemaining": 3 } } Standard Error Types : 400 BAD_REQUEST : Google token is invalid, expired, or email already linked to another account 401 UNAUTHORIZED : Missing or invalid Bearer token 422 UNPROCESSABLE_ENTITY : idToken is blank Flow Overview Primary Onboarding Complete โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Set Username โ”‚ POST /username โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Link Email โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ Option A: Custom โ”‚ POST /email/custom/initiate โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ†’ POST /email/custom/verify โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ Option B: Google โ”‚ POST /email/google โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Profile Pic โ”‚ POST /profile-pic โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Interests โ”‚ POST /interests (min. 3) โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Bio โ”‚ POST /bio โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ action: PROCEED (onboarding done) Steps can be completed out of order. The nextMissing field in each response always signals the next recommended incomplete step. Quick Reference # Endpoint Method Auth Purpose 1 /username/suggestions GET Bearer Get suggested usernames 2 /username POST Bearer Set username 3 /bio POST Bearer Set bio 4 /interests POST Bearer Set interests 5 /profile-pic POST Bearer Upload profile picture 6 /email/custom/initiate POST Bearer Send OTP to email 7 /email/custom/verify POST Bearer Verify email with OTP 8 /email/google POST Bearer Link email via Google token