# 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:

1. **Passwordless-First**: Users authenticate primarily via OTP, with password as an optional secondary method
2. **Device-Aware Security**: Every login tracks device information for risk assessment
3. **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

<table id="bkmrk-token-purpose-expiry"><thead><tr><th>Token</th><th>Purpose</th><th>Expiry</th><th>Storage</th></tr></thead><tbody><tr><td>`tempToken`</td><td>OTP verification, flow state</td><td>10 min</td><td>Memory only</td></tr><tr><td>`onboardingToken`</td><td>Onboarding step progression</td><td>30 min</td><td>Memory only</td></tr><tr><td>`onboardingRefreshToken`</td><td>Refresh onboarding token</td><td>24 hours</td><td>Secure storage</td></tr><tr><td>`accessToken`</td><td>API authentication</td><td>1 hour</td><td>Secure storage</td></tr><tr><td>`refreshToken`</td><td>Get new access tokens</td><td>30 days</td><td>Secure storage</td></tr><tr><td>`deviceVerificationToken`</td><td>New device verification</td><td>10 min</td><td>Memory only</td></tr></tbody></table>

## Onboarding Steps Explained

<table id="bkmrk-step-enum-value-what"><thead><tr><th>Step</th><th>Enum Value</th><th>What Happens</th><th>Metadata Provided</th></tr></thead><tbody><tr><td>1</td><td>`INITIATED`</td><td>Account created, OTP sent</td><td>-</td></tr><tr><td>2</td><td>`OTP_VERIFIED`</td><td>OTP confirmed, ready for age</td><td>`firstName`, `lastName`, `profilePicture` (from OAuth)</td></tr><tr><td>3</td><td>`AGE_VERIFIED`</td><td>Age confirmed, names saved</td><td>`suggestedUsernames` (5 smart suggestions)</td></tr><tr><td>4</td><td>`USERNAME_SET`</td><td>Username chosen</td><td>-</td></tr><tr><td>5</td><td>`INTERESTS_SELECTED`</td><td>Interests saved</td><td>`firstName`, `lastName`, `username`, `suggestedDisplayName`, `profilePicture`</td></tr><tr><td>6</td><td>`COMPLETED`</td><td>Profile complete, tokens issued</td><td>-</td></tr></tbody></table>

---

# 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

```bash
npm install @fingerprintjs/fingerprintjs

```

### React Implementation

```javascript
// 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

```javascript
// 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)

```javascript
// 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

```javascript
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:

```javascript
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**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-01-24T10:30:45",
  "data": { }
}

```

**Error Response Structure**:

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-01-24T10:30:45",
  "data": "Error description"
}

```

**Standard Response Fields**:

<table id="bkmrk-field-type-descripti"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`success`</td><td>boolean</td><td>`true` for successful operations, `false` for errors</td></tr><tr><td>`httpStatus`</td><td>string</td><td>HTTP status name (OK, BAD\_REQUEST, NOT\_FOUND, etc.)</td></tr><tr><td>`message`</td><td>string</td><td>Human-readable message</td></tr><tr><td>`action_time`</td><td>string</td><td>ISO 8601 timestamp</td></tr><tr><td>`data`</td><td>object/string</td><td>Response payload or error details</td></tr></tbody></table>

---

# API Endpoints

## Authentication Initialization

### 1. Start Authentication

**Purpose**: Initiate authentication flow for phone, email, or username

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/start`

**Access Level**: 🌐 Public

**Authentication**: None

**Request JSON Sample**:

```json
{
  "identifier": "+255712345678",
  "deviceId": "a1b2c3d4e5f6789",
  "deviceName": "Chrome on macOS",
  "platform": "WEB"
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>identifier</td><td>string</td><td>Yes</td><td>Phone (+255...), email, or @username</td><td>Valid format</td></tr><tr><td>deviceId</td><td>string</td><td>Yes</td><td>Device fingerprint</td><td>Non-empty</td></tr><tr><td>deviceName</td><td>string</td><td>Yes</td><td>Human-readable device name</td><td>Non-empty</td></tr><tr><td>platform</td><td>string</td><td>Yes</td><td>Device platform</td><td>enum: `IOS`, `ANDROID`, `WEB`</td></tr></tbody></table>

**Success Response JSON Sample** (New User):

```json
{
  "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):

```json
{
  "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):

```json
{
  "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**:

<table id="bkmrk-field-description-te"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>tempToken</td><td>Temporary token for next step</td></tr><tr><td>maskedIdentifier</td><td>Masked phone/email for display</td></tr><tr><td>provider</td><td>Identifier type: `PHONE`, `EMAIL`</td></tr><tr><td>otpExpiresIn</td><td>OTP validity in seconds (600 = 10 minutes)</td></tr><tr><td>isNewUser</td><td>Whether this is a new registration</td></tr><tr><td>onboardingComplete</td><td>Whether user completed onboarding</td></tr><tr><td>currentStep</td><td>Current onboarding step</td></tr><tr><td>hasPassword</td><td>Whether user has set a password</td></tr><tr><td>requiresOtpChannelSelection</td><td>If true, user must choose OTP channel</td></tr><tr><td>availableChannels</td><td>List of available OTP channels</td></tr></tbody></table>

**Error Responses**:

*Identifier Blocked (400):*

```json
{
  "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):*

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/verify`

**Access Level**: 🌐 Public

**Authentication**: None

**Request JSON Sample**:

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "otp": "123456",
  "deviceId": "a1b2c3d4e5f6789",
  "deviceName": "Chrome on macOS",
  "platform": "WEB"
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-1"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>tempToken</td><td>string</td><td>Yes</td><td>Token from /auth/start</td><td>Valid JWT</td></tr><tr><td>otp</td><td>string</td><td>Yes</td><td>6-digit OTP code</td><td>Exactly 6 digits</td></tr><tr><td>deviceId</td><td>string</td><td>No</td><td>Device fingerprint</td><td>-</td></tr><tr><td>deviceName</td><td>string</td><td>No</td><td>Human-readable device name</td><td>-</td></tr><tr><td>platform</td><td>string</td><td>No</td><td>Device platform</td><td>enum: `IOS`, `ANDROID`, `WEB`</td></tr></tbody></table>

**Success Response JSON Sample** (New User - Start Onboarding):

```json
{
  "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):

```json
{
  "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):

```json
{
  "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):*

```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Invalid OTP code",
  "action_time": "2025-01-24T10:30:45",
  "data": "Invalid OTP code"
}

```

*Max Attempts Exceeded (403):*

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/send-otp-to-channel`

**Access Level**: 🌐 Public

**Authentication**: None

**Request JSON Sample**:

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "channel": "EMAIL"
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-2"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>tempToken</td><td>string</td><td>Yes</td><td>Token from /auth/start</td><td>Valid JWT</td></tr><tr><td>channel</td><td>string</td><td>Yes</td><td>Preferred OTP channel</td><td>enum: `EMAIL`, `PHONE`</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/login/password`

**Access Level**: 🌐 Public

**Authentication**: None

**Request JSON Sample**:

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "password": "mySecurePassword123",
  "deviceId": "a1b2c3d4e5f6789",
  "deviceName": "Chrome on macOS",
  "platform": "WEB"
}

```

**Success Response JSON Sample** (Known Device):

```json
{
  "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):

```json
{
  "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):*

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/login/oauth`

**Access Level**: 🌐 Public

**Authentication**: None

**Request JSON Sample**:

```json
{
  "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**:

<table id="bkmrk-parameter-type-requi-3"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>provider</td><td>string</td><td>Yes</td><td>OAuth provider</td><td>enum: `GOOGLE`, `APPLE`</td></tr><tr><td>code</td><td>string</td><td>Yes</td><td>Authorization code</td><td>Non-empty</td></tr><tr><td>redirectUri</td><td>string</td><td>Yes</td><td>Redirect URI</td><td>Must match OAuth config</td></tr><tr><td>state</td><td>string</td><td>No</td><td>State for CSRF protection</td><td>-</td></tr><tr><td>deviceId</td><td>string</td><td>No</td><td>Device fingerprint</td><td>-</td></tr><tr><td>deviceName</td><td>string</td><td>No</td><td>Device name</td><td>-</td></tr><tr><td>platform</td><td>string</td><td>No</td><td>Device platform</td><td>enum: `IOS`, `ANDROID`, `WEB`</td></tr></tbody></table>

**Success Response JSON Sample** (New User):

```json
{
  "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):

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/device/verify`

**Access Level**: 🌐 Public

**Authentication**: None

**Request JSON Sample**:

```json
{
  "deviceVerificationToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "otp": "123456",
  "deviceId": "a1b2c3d4e5f6789",
  "deviceName": "Chrome on macOS",
  "platform": "WEB"
}

```

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/resend-otp`

**Access Level**: 🌐 Public

**Authentication**: None

**Request JSON Sample**:

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

```

**Success Response JSON Sample**:

```json
{
  "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**:

```json
{
  "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**:

```json
{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "firstName": "John",
  "lastName": "Doe",
  "birthDate": "1999-05-15"
}

```

<table id="bkmrk-parameter-type-requi-4"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>onboardingToken</td><td>string</td><td>Yes</td><td>Token from previous step</td><td>Valid JWT</td></tr><tr><td>firstName</td><td>string</td><td>Yes</td><td>User's first name</td><td>1-50 characters</td></tr><tr><td>lastName</td><td>string</td><td>Yes</td><td>User's last name</td><td>1-50 characters</td></tr><tr><td>birthDate</td><td>string</td><td>Yes</td><td>Date of birth</td><td>ISO date (YYYY-MM-DD), must be in past</td></tr></tbody></table>

**Success Response**:

```json
{
  "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):

```json
{
  "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 &lt; 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**:

```json
{
  "username": "johndoe99"
}

```

**Response - Available**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Username availability checked",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "username": "johndoe99",
    "available": true
  }
}

```

**Response - Not Available**:

```json
{
  "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**:

```json
{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "username": "johndoe99"
}

```

<table id="bkmrk-parameter-type-requi-5"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>onboardingToken</td><td>string</td><td>Yes</td><td>Token from previous step</td><td>Valid JWT</td></tr><tr><td>username</td><td>string</td><td>Yes</td><td>Chosen username</td><td>3-30 chars, starts with letter, alphanumeric + underscore</td></tr></tbody></table>

**Success Response**:

```json
{
  "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 -&gt; must verify `EMAIL`
> - Email user -&gt; must verify `PHONE`
> - Google/Apple user -&gt; 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**:

```json
{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "contactValue": "johndoe@gmail.com"
}

```

<table id="bkmrk-parameter-type-requi-6"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>onboardingToken</td><td>string</td><td>Yes</td><td>Token from previous step</td><td>Valid JWT</td></tr><tr><td>contactValue</td><td>string</td><td>Conditional</td><td>Email or phone to verify</td><td>Required only if `requiresInput = true`</td></tr></tbody></table>

**Success Response**:

```json
{
  "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**:

```json
{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "otp": "847291"
}

```

<table id="bkmrk-parameter-type-requi-7"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>onboardingToken</td><td>string</td><td>Yes</td><td>Current onboarding token</td></tr><tr><td>tempToken</td><td>string</td><td>Yes</td><td>Token returned from initiate endpoint</td></tr><tr><td>otp</td><td>string</td><td>Yes</td><td>6-digit OTP code</td></tr></tbody></table>

**Success Response**:

```json
{
  "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**:

```json
{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "newContactValue": "newemail@gmail.com"
}

```

<table id="bkmrk-parameter-type-requi-8"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>onboardingToken</td><td>string</td><td>Yes</td><td>Current onboarding token</td></tr><tr><td>tempToken</td><td>string</td><td>Yes</td><td>Token from initiate endpoint</td></tr><tr><td>newContactValue</td><td>string</td><td>Yes</td><td>New email or phone number</td></tr></tbody></table>

**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 -&gt; hard block, error returned
- Taken by an **unverified** account -&gt; released automatically, current user proceeds
- Same account re-entering same contact -&gt; 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**:

```json
{
  "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**:

```json
{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "interestIds": [
    "550e8400-e29b-41d4-a716-446655440001",
    "550e8400-e29b-41d4-a716-446655440002",
    "550e8400-e29b-41d4-a716-446655440003"
  ]
}

```

<table id="bkmrk-parameter-type-requi-9"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>onboardingToken</td><td>string</td><td>Yes</td><td>Token from previous step</td><td>Valid JWT</td></tr><tr><td>interestIds</td><td>array</td><td>Yes</td><td>List of category UUIDs</td><td>Min 3 items</td></tr></tbody></table>

**Success Response**:

```json
{
  "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**:

<table id="bkmrk-header-type-required"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>X-Device-Id</td><td>string</td><td>No</td><td>Device fingerprint</td></tr><tr><td>X-Device-Name</td><td>string</td><td>No</td><td>Device name</td></tr><tr><td>X-Platform</td><td>string</td><td>No</td><td>`IOS`, `ANDROID`, or `WEB`</td></tr></tbody></table>

**Request Body**:

```json
{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "displayName": "John Doe",
  "bio": "Tech enthusiast | Music lover | Based in Dar es Salaam 🇹🇿",
  "profilePictureUrl": "https://storage.nextgate.com/profiles/abc123.jpg"
}

```

<table id="bkmrk-parameter-type-requi-10"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>onboardingToken</td><td>string</td><td>Yes</td><td>Token from previous step</td><td>Valid JWT</td></tr><tr><td>displayName</td><td>string</td><td>Yes</td><td>Public display name</td><td>1-50 characters</td></tr><tr><td>bio</td><td>string</td><td>No</td><td>User biography</td><td>Max 160 characters</td></tr><tr><td>profilePictureUrl</td><td>string</td><td>No</td><td>Profile picture URL</td><td>Valid URL</td></tr></tbody></table>

**Success Response**:

```json
{
  "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`

<table id="bkmrk-parameter-type-requi-11"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>file</td><td>file</td><td>Yes</td><td>Profile picture file</td><td>Image files only, max 25MB</td></tr><tr><td>X-Onboarding-Token</td><td>header</td><td>Yes</td><td>Token from previous step</td><td>Valid onboarding JWT</td></tr></tbody></table>

**Success Response**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/token/refresh`

**Access Level**: 🌐 Public

**Authentication**: None (refresh token in body)

**Request JSON Sample**:

```json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

```

**Success Response JSON Sample**:

```json
{
  "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):

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/token/revoke`

**Access Level**: 🌐 Public

**Authentication**: None

**Request JSON Sample**:

```json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

```

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/token/refresh-onboarding`

**Access Level**: 🌐 Public

**Authentication**: None

**Request JSON Sample**:

```json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

```

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/password/forgot/initiate`

**Access Level**: 🌐 Public

**Authentication**: None

**Request JSON Sample**:

```json
{
  "identifier": "johndoe99"
}

```

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/password/forgot/verify-otp`

**Access Level**: 🌐 Public

**Authentication**: None

**Request JSON Sample**:

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "otp": "123456"
}

```

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/password/forgot/reset`

**Access Level**: 🌐 Public

**Authentication**: None

**Request JSON Sample**:

```json
{
  "resetToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "newPassword": "newSecurePassword123",
  "confirmPassword": "newSecurePassword123"
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-12"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>resetToken</td><td>string</td><td>Yes</td><td>Token from verify-otp</td><td>Valid JWT</td></tr><tr><td>newPassword</td><td>string</td><td>Yes</td><td>New password</td><td>Min 8 characters</td></tr><tr><td>confirmPassword</td><td>string</td><td>Yes</td><td>Confirmation</td><td>Must match newPassword</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/password/change`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-1"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>`Bearer {accessToken}`</td></tr></tbody></table>

**Request JSON Sample**:

```json
{
  "currentPassword": "oldPassword123",
  "newPassword": "newSecurePassword456",
  "confirmPassword": "newSecurePassword456"
}

```

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/password/set`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

```json
{
  "newPassword": "myNewPassword123",
  "confirmPassword": "myNewPassword123"
}

```

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/auth/sessions`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-2"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>`Bearer {accessToken}`</td></tr><tr><td>X-Session-Id</td><td>string</td><td>No</td><td>Current session ID</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/sessions/sign-out`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

```json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

```

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/sessions/sign-out-all`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `{base_url}/auth/sessions/{sessionId}`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-13"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>sessionId</td><td>string</td><td>Yes</td><td>Session UUID to revoke</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/auth/devices`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-3"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>`Bearer {accessToken}`</td></tr><tr><td>X-Device-Id</td><td>string</td><td>No</td><td>Current device ID</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `{base_url}/auth/devices/{deviceId}`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-14"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>deviceId</td><td>string</td><td>Yes</td><td>Device record UUID</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/auth/linked-accounts`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/linked-accounts/email/link`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

```json
{
  "email": "john.work@company.com"
}

```

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/linked-accounts/phone/link`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

```json
{
  "phone": "+255787654321"
}

```

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/linked-accounts/oauth/link`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

```json
{
  "provider": "APPLE",
  "code": "authorization_code_from_apple",
  "redirectUri": "https://app.nextgate.com/auth/callback"
}

```

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `{base_url}/auth/linked-accounts/oauth/{provider}`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-15"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>provider</td><td>string</td><td>Yes</td><td>`GOOGLE` or `APPLE`</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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):

```json
{
  "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

<table id="bkmrk-http-code-status-com"><thead><tr><th>HTTP Code</th><th>Status</th><th>Common Causes</th></tr></thead><tbody><tr><td>400</td><td>BAD\_REQUEST</td><td>Invalid request data, item already exists</td></tr><tr><td>401</td><td>UNAUTHORIZED</td><td>Missing/invalid/expired token</td></tr><tr><td>403</td><td>FORBIDDEN</td><td>Invalid OTP, max attempts exceeded, blocked</td></tr><tr><td>404</td><td>NOT\_FOUND</td><td>Resource doesn't exist</td></tr><tr><td>422</td><td>UNPROCESSABLE\_ENTITY</td><td>Validation errors</td></tr><tr><td>429</td><td>TOO\_MANY\_REQUESTS</td><td>Rate limit exceeded</td></tr><tr><td>500</td><td>INTERNAL\_SERVER\_ERROR</td><td>Server error</td></tr></tbody></table>

## Authentication-Specific Errors

<table id="bkmrk-error-message-http-c"><thead><tr><th>Error Message</th><th>HTTP Code</th><th>Resolution</th></tr></thead><tbody><tr><td>"Token has expired"</td><td>401</td><td>Refresh token or re-authenticate</td></tr><tr><td>"Invalid OTP code"</td><td>403</td><td>Re-enter correct code</td></tr><tr><td>"Maximum verification attempts exceeded"</td><td>403</td><td>Request new OTP</td></tr><tr><td>"This phone is blocked"</td><td>400</td><td>Contact support</td></tr><tr><td>"Account not found"</td><td>404</td><td>Check identifier</td></tr><tr><td>"Security alert: Token reuse detected"</td><td>401</td><td>Login again</td></tr><tr><td>"Login blocked: Too many failed attempts"</td><td>400</td><td>Wait or reset password</td></tr><tr><td>"You must be at least 13 years old"</td><td>N/A</td><td>Cannot use service</td></tr></tbody></table>

---

# 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

1. **Never store tokens in localStorage** - Use httpOnly cookies or secure native storage
2. **Always send device fingerprint** - Required for device trust tracking
3. **Handle token rotation** - Always save new refresh token after refresh
4. **Validate OTP client-side** - Only allow 6 digits before API call
5. **Rate limit on frontend** - Disable resend button during cooldown
6. **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:

```json
{
  "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

<table id="bkmrk-aspect-ios-android-w"><thead><tr><th>Aspect</th><th>iOS</th><th>Android</th><th>Web</th></tr></thead><tbody><tr><td>**Key Storage**</td><td>Secure Enclave</td><td>StrongBox / TEE</td><td>Memory only</td></tr><tr><td>**Hardware Protected**</td><td>✅ Yes</td><td>✅ Yes</td><td>❌ No</td></tr><tr><td>**Key Extractable**</td><td>❌ Never</td><td>❌ Never</td><td>N/A (ephemeral)</td></tr><tr><td>**Forgery Possible**</td><td>❌ No</td><td>❌ No</td><td>⚠️ Harder</td></tr><tr><td>**Trust Level**</td><td>⭐⭐⭐⭐⭐</td><td>⭐⭐⭐⭐⭐</td><td>⭐⭐⭐</td></tr><tr><td>**Trust Duration**</td><td>30 days</td><td>30 days</td><td>7 days</td></tr><tr><td>**Sensitive Actions**</td><td>Some without OTP</td><td>Some without OTP</td><td>ALWAYS OTP</td></tr></tbody></table>

---

### 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:**

```json
{
  "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:**

```json
{
  "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...",
  "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
  "timestamp": 1736611200000,
  "signature": "MEUCIQC7...",
  "platform": "IOS"
}

```

<table id="bkmrk-field-description-de"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>`deviceId`</td><td>SHA256 hash of public key, prefixed with platform</td></tr><tr><td>`publicKey`</td><td>Base64-encoded ECDSA P-256 public key</td></tr><tr><td>`nonce`</td><td>Challenge from `/auth/challenge`</td></tr><tr><td>`timestamp`</td><td>Current time in milliseconds</td></tr><tr><td>`signature`</td><td>Sign(`nonce|timestamp|deviceId`, privateKey)</td></tr><tr><td>`platform`</td><td>`IOS`, `ANDROID`, or `WEB`</td></tr></tbody></table>

**Response (Success):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...",
  "fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c"
}

```

<table id="bkmrk-field-description-pu"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>`publicKey`</td><td>Ephemeral public key (generated in memory)</td></tr><tr><td>`fingerprint`</td><td>Browser fingerprint hash</td></tr></tbody></table>

**Response:**

```json
{
  "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:**

```json
{
  "identifier": "alex@example.com",
  "password": "MySecurePass123!",
  "deviceAuth": {
    "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
    "timestamp": 1736611200000,
    "signature": "MEUCIQC7...",
    "platform": "IOS"
  }
}

```

**Response (Success - Trusted Device):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456",
  "trustDevice": true,
  "deviceAuth": {
    "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "nonce": "ch_newNonceForVerification",
    "timestamp": 1736611260000,
    "signature": "MEUCIQD8...",
    "platform": "IOS"
  }
}

```

**Response (Success):**

```json
{
  "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):**

```json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
  "deviceAuth": {
    "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "nonce": "ch_refreshNonce123",
    "timestamp": 1736614800000,
    "signature": "MEUCIQDx...",
    "platform": "IOS"
  }
}

```

**Request (Web):**

```json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
  "webAuth": {
    "sessionId": "ws_abc123xyz789",
    "nonce": "ch_refreshNonce123",
    "timestamp": 1736614800000,
    "signature": "MEUCIQDx...",
    "fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c"
  }
}

```

**Response:**

```json
{
  "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):**

```json
{
  "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

```sql
-- 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.

<table id="bkmrk-field-type-purpose-c"><thead><tr><th>Field</th><th>Type</th><th>Purpose</th><th>Can Change?</th><th>Used In</th></tr></thead><tbody><tr><td>`id`</td><td>UUID</td><td>Primary key</td><td>❌ Never</td><td>DB relations</td></tr><tr><td>`systemUsername`</td><td>String</td><td>Internal identifier</td><td>❌ Never</td><td>JWT tokens, internal APIs</td></tr><tr><td>`userName`</td><td>String</td><td>Public @handle</td><td>✅ Yes</td><td>Profile URL, mentions, search, display</td></tr></tbody></table>

**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:**

```sql
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

<table id="bkmrk-auth-method-compromi"><thead><tr><th>Auth Method Compromised</th><th>Can Attacker Access Account?</th></tr></thead><tbody><tr><td>Password leaked</td><td>❌ No - needs device OTP or phone OTP</td></tr><tr><td>Email hacked</td><td>❌ No - sensitive actions need phone OTP</td></tr><tr><td>Google hacked</td><td>❌ No - new device needs phone OTP</td></tr><tr><td>Apple hacked</td><td>❌ No - new device needs phone OTP</td></tr><tr><td>Phone stolen (unlocked)</td><td>⚠️ Partial - but needs password for sensitive actions</td></tr><tr><td>Phone + Password both</td><td>✅ Yes - full access (this is expected)</td></tr></tbody></table>

### 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 &amp; MERGING

### When OAuth Email Matches Existing Account

**Scenario Matrix:**

<table id="bkmrk-existing-account-sta"><thead><tr><th>Existing Account State</th><th>OAuth Provider</th><th>Action</th></tr></thead><tbody><tr><td>Email verified + password</td><td>Google (same email)</td><td>Prompt to link</td></tr><tr><td>Email verified + no password</td><td>Google (same email)</td><td>Prompt to link</td></tr><tr><td>Email unverified</td><td>Google (same email)</td><td>Auto-link (Google verified it)</td></tr><tr><td>Phone only (no email set)</td><td>Google (new email)</td><td>Prompt to add email or create new</td></tr><tr><td>Email verified but different</td><td>Google (different email)</td><td>Create new account</td></tr></tbody></table>

### Link Account Flow

**POST** `/api/v1/auth/oauth/google`

**Request:**

```json
{
  "idToken": "eyJhbGciOiJSUzI1NiIs...",
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS"
  }
}

```

**Response (Email Matches Existing - Link Required):**

```json
{
  "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:**

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "method": "PASSWORD",
  "password": "MySecurePass123!"
}

```

**Response (Success):**

```json
{
  "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:**

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "method": "OTP_PHONE"
}

```

**Response:**

```json
{
  "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:**

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "method": "OTP_PHONE",
  "otpCode": "123456"
}

```

---

### Auto-Link (Unverified Email)

When existing account has unverified email that matches Google email:

**Response (Auto-Linked):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "verificationMethod": "OTP_PHONE",
  "otpCode": "123456"
}

```

**Response (Success):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "provider": "APPLE",
  "identityToken": "eyJraWQiOiJXNldjT0...",
  "authorizationCode": "c7e8a3f1b2d4..."
}

```

**Response:**

```json
{
  "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:**

```json
{
  "identifier": "alexvibes",
  "recoveryReason": "LOST_PHONE",
  "contactEmail": "backup@anotheremail.com"
}

```

**Response:**

```json
{
  "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

<table id="bkmrk-action-otp-required%3F"><thead><tr><th>Action</th><th>OTP Required?</th><th>Additional Verification</th></tr></thead><tbody><tr><td>Change password</td><td>✅ Phone OTP</td><td>Current password (if set)</td></tr><tr><td>Change email</td><td>✅ Phone OTP</td><td>-</td></tr><tr><td>Change phone</td><td>✅ Old phone OTP + New phone OTP</td><td>-</td></tr><tr><td>Link OAuth provider</td><td>✅ Phone OTP</td><td>-</td></tr><tr><td>Unlink OAuth provider</td><td>✅ Phone OTP</td><td>-</td></tr><tr><td>Delete account</td><td>✅ Phone OTP</td><td>Password (if set)</td></tr><tr><td>View full payment methods</td><td>✅ Phone OTP</td><td>-</td></tr><tr><td>Add payment method</td><td>❌ No</td><td>-</td></tr><tr><td>Remove payment method</td><td>✅ Phone OTP</td><td>-</td></tr><tr><td>Large purchase (&gt;$100)</td><td>⚙️ Configurable</td><td>-</td></tr><tr><td>Export account data</td><td>✅ Phone OTP</td><td>-</td></tr><tr><td>Change security settings</td><td>✅ Phone OTP</td><td>Password (if set)</td></tr></tbody></table>

### Request OTP for Sensitive Action

**POST** `/api/v1/auth/sensitive-action/request-otp`

**Request:**

```json
{
  "action": "CHANGE_EMAIL",
  "newEmail": "newemail@example.com"
}

```

**Response:**

```json
{
  "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:**

```json
{
  "actionToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456"
}

```

**Response:**

```json
{
  "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)

```json
{
  "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 &amp; SECURITY

### Complete Auth Provider Matrix

<table id="bkmrk-provider-can-signup%3F"><thead><tr><th>Provider</th><th>Can Signup?</th><th>Can Login?</th><th>Provides Email?</th><th>Provides Phone?</th><th>Trust Level</th></tr></thead><tbody><tr><td>Phone + OTP</td><td>✅</td><td>✅</td><td>❌</td><td>✅</td><td>HIGH</td></tr><tr><td>Email + OTP</td><td>✅</td><td>✅</td><td>✅</td><td>❌</td><td>MEDIUM</td></tr><tr><td>Email + Password</td><td>❌ (need OTP first)</td><td>✅</td><td>✅</td><td>❌</td><td>MEDIUM</td></tr><tr><td>Google OAuth</td><td>✅</td><td>✅</td><td>✅</td><td>❌</td><td>MEDIUM</td></tr><tr><td>Apple OAuth</td><td>✅</td><td>✅</td><td>✅ (may be relay)</td><td>❌</td><td>MEDIUM</td></tr><tr><td>Password only</td><td>❌</td><td>✅ (trusted device)</td><td>❌</td><td>❌</td><td>LOW</td></tr></tbody></table>

### Security Recommendations for Users

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "method": "EMAIL",
  "email": "alex@example.com"
}

```

**Response (Success):**

```json
{
  "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:**

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456"
}

```

**Response (Success - Account Created):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIs..."
}

```

**Response (Success):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..."
}

```

**Response (Success - New User):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "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 &amp; Birthdate

### 2.1 Save Name &amp; Birthdate

**PUT** `/api/v1/onboarding/name-birthdate`

**Headers:**

```
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

```

**Request:**

```json
{
  "displayName": "Alex Johnson",
  "firstName": "Alex",
  "lastName": "Johnson",
  "birthDate": "1995-06-15"
}

```

**Response (Success):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "userName": "alexvibes",
  "bio": "Fashion lover | Shopaholic | Event creator 🔥",
  "profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380.jpg"
}

```

**Response (Success):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "categoryIds": [
    "cat_001",
    "cat_003",
    "cat_006",
    "cat_009",
    "cat_012"
  ]
}

```

**Response (Success):**

```json
{
  "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):**

```json
{
  "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:**

```json
{}

```

**Response:**

```json
{
  "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 &amp; Suggestions

**GET** `/api/v1/onboarding/complete`

**Headers:**

```
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

```

**Response:**

```json
{
  "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:**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "idToken": "eyJhbGciOiJSUzI1NiIs...",
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS",
    "appVersion": "1.2.0"
  }
}

```

**Response (Existing User - Trusted Device):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456",
  "trustDevice": true,
  "deviceInfo": {
    "deviceId": "xyz789-new-device",
    "deviceName": "Chrome on Windows",
    "deviceType": "WEB_BROWSER"
  }
}

```

**Response (Success):**

```json
{
  "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:**

```json
{
  "newPassword": "MySecurePass123!",
  "confirmPassword": "MySecurePass123!"
}

```

**Response (Success):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

```

**Response (Success):**

```json
{
  "success": true,
  "message": "Token refreshed successfully",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600
  },
  "timestamp": "2025-01-11T17:00:00Z"
}

```

**Response (Invalid/Expired Refresh Token):**

```json
{
  "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:**

```json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
  "logoutAllDevices": false
}

```

**Response:**

```json
{
  "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:**

```json
{
  "identifier": "alex@example.com"
}

```

**Response:**

```json
{
  "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 &amp; Set New Password

**POST** `/api/v1/auth/password/reset/confirm`

**Request:**

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456",
  "newPassword": "MyNewSecurePass123!",
  "confirmPassword": "MyNewSecurePass123!"
}

```

**Response:**

```json
{
  "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:**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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:

```json
{
  "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

<table id="bkmrk-code-http-status-des"><thead><tr><th>Code</th><th>HTTP Status</th><th>Description</th></tr></thead><tbody><tr><td>`VALIDATION_ERROR`</td><td>BAD\_REQUEST</td><td>Request validation failed</td></tr><tr><td>`INVALID_OTP`</td><td>BAD\_REQUEST</td><td>OTP code is incorrect</td></tr><tr><td>`OTP_EXPIRED`</td><td>BAD\_REQUEST</td><td>OTP has expired</td></tr><tr><td>`INVALID_CREDENTIALS`</td><td>UNAUTHORIZED</td><td>Wrong password</td></tr><tr><td>`TOKEN_EXPIRED`</td><td>UNAUTHORIZED</td><td>Access token expired</td></tr><tr><td>`REFRESH_TOKEN_EXPIRED`</td><td>UNAUTHORIZED</td><td>Refresh token expired</td></tr><tr><td>`UNAUTHORIZED`</td><td>UNAUTHORIZED</td><td>Not authenticated</td></tr><tr><td>`FORBIDDEN`</td><td>FORBIDDEN</td><td>Not allowed</td></tr><tr><td>`USER_NOT_FOUND`</td><td>NOT\_FOUND</td><td>User doesn't exist</td></tr><tr><td>`ACCOUNT_EXISTS`</td><td>CONFLICT</td><td>Account already exists</td></tr><tr><td>`USERNAME_TAKEN`</td><td>CONFLICT</td><td>Username is taken</td></tr><tr><td>`PHONE_ALREADY_REGISTERED`</td><td>CONFLICT</td><td>Phone belongs to another account</td></tr><tr><td>`PHONE_TEMPORARILY_RESERVED`</td><td>CONFLICT</td><td>Phone claimed but unverified</td></tr><tr><td>`ACCOUNT_LOCKED`</td><td>LOCKED</td><td>Too many failed attempts</td></tr><tr><td>`RESEND_COOLDOWN`</td><td>TOO\_MANY\_REQUESTS</td><td>Wait before resending OTP</td></tr><tr><td>`RATE_LIMITED`</td><td>TOO\_MANY\_REQUESTS</td><td>Too many requests</td></tr><tr><td>`WEAK_PASSWORD`</td><td>UNPROCESSABLE\_ENTITY</td><td>Password doesn't meet requirements</td></tr><tr><td>`UNDERAGE`</td><td>UNPROCESSABLE\_ENTITY</td><td>User is under minimum age</td></tr><tr><td>`MIN_INTERESTS_REQUIRED`</td><td>UNPROCESSABLE\_ENTITY</td><td>Must have minimum interests</td></tr><tr><td>`MAX_INTERESTS_REACHED`</td><td>UNPROCESSABLE\_ENTITY</td><td>Maximum interests reached</td></tr><tr><td>`SERVER_ERROR`</td><td>INTERNAL\_SERVER\_ERROR</td><td>Internal server error</td></tr></tbody></table>

---

## 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:

1. **Explicit**: User picks during onboarding (visible to user)
2. **Implicit**: Silent tracking based on user behavior (invisible to user)

Scores update silently in the background as users interact with content.

---

### Interest Scoring Logic

<table id="bkmrk-action-score-notes-e"><thead><tr><th>Action</th><th>Score</th><th>Notes</th></tr></thead><tbody><tr><td>Explicitly selected (onboarding)</td><td>+100</td><td>Base score for picked interests</td></tr><tr><td>View post</td><td>+1</td><td>Quick glance</td></tr><tr><td>Like post</td><td>+3</td><td>Shows interest</td></tr><tr><td>Comment on post</td><td>+5</td><td>Higher engagement</td></tr><tr><td>Share post</td><td>+7</td><td>Strong signal</td></tr><tr><td>Save/Bookmark</td><td>+5</td><td>Wants to revisit</td></tr><tr><td>Follow user in category</td><td>+10</td><td>Committed interest</td></tr><tr><td>View product</td><td>+2</td><td>Shopping interest</td></tr><tr><td>Add to cart</td><td>+8</td><td>Purchase intent</td></tr><tr><td>Purchase product</td><td>+15</td><td>Strongest signal</td></tr><tr><td>View event</td><td>+2</td><td>Curious</td></tr><tr><td>Attend event</td><td>+12</td><td>Committed</td></tr><tr><td>Search term</td><td>+4</td><td>Active seeking</td></tr><tr><td>Time spent (per 30sec)</td><td>+1</td><td>Passive engagement</td></tr><tr><td>Scroll past quickly</td><td>-1</td><td>Not interested</td></tr><tr><td>Hide/Not interested</td><td>-20</td><td>Explicit dislike</td></tr><tr><td>Report content</td><td>-30</td><td>Strong negative</td></tr></tbody></table>

**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:**

```json
{
  "name": "Fashion",
  "icon": "👗",
  "iconUrl": "https://cdn.example.com/icons/fashion.png",
  "color": "#FF6B6B",
  "keywords": ["clothes", "style", "outfit", "wear", "dress"],
  "displayOrder": 1,
  "isActive": true
}

```

**Response:**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "order": [
    { "categoryId": "cat_003", "displayOrder": 1 },
    { "categoryId": "cat_001", "displayOrder": 2 },
    { "categoryId": "cat_002", "displayOrder": 3 }
  ]
}

```

**Response:**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "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 &amp; Settings)

**GET** `/api/v1/interests/categories`

*No auth required for onboarding, shows only active categories*

**Response:**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "categoryIds": ["cat_001", "cat_003", "cat_006", "cat_010", "cat_012"]
}

```

**Response:**

```json
{
  "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:**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "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:

1. Gets the category tags from that content
2. Calculates score based on action type
3. Updates user's implicit interest scores silently
4. No API call from client needed

### Actions That Trigger Tracking

<table id="bkmrk-user-action-system-t"><thead><tr><th>User Action</th><th>System Tracks</th></tr></thead><tbody><tr><td>Views post</td><td>`POST_VIEW` on post's categories</td></tr><tr><td>Likes post</td><td>`POST_LIKE` on post's categories</td></tr><tr><td>Comments on post</td><td>`POST_COMMENT` on post's categories</td></tr><tr><td>Shares post</td><td>`POST_SHARE` on post's categories</td></tr><tr><td>Saves post</td><td>`POST_SAVE` on post's categories</td></tr><tr><td>Views product</td><td>`PRODUCT_VIEW` on product's category</td></tr><tr><td>Adds to cart</td><td>`PRODUCT_CART` on product's category</td></tr><tr><td>Purchases</td><td>`PRODUCT_PURCHASE` on product's category</td></tr><tr><td>Views shop</td><td>`SHOP_VIEW` on shop's categories</td></tr><tr><td>Follows shop</td><td>`SHOP_FOLLOW` on shop's categories</td></tr><tr><td>Views event</td><td>`EVENT_VIEW` on event's categories</td></tr><tr><td>RSVPs to event</td><td>`EVENT_RSVP` on event's categories</td></tr><tr><td>Follows user</td><td>`USER_FOLLOW` on user's primary categories</td></tr><tr><td>Searches</td><td>`SEARCH` on matched categories</td></tr><tr><td>Scrolls past quickly</td><td>`SCROLL_PAST` (negative)</td></tr><tr><td>Clicks "Not interested"</td><td>`HIDE_CONTENT` (strong negative)</td></tr></tbody></table>

### Content Must Have Category Tags

For tracking to work, all content must be tagged:

```json
// 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:**

```json
{
  "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

<table id="bkmrk-method-endpoint-desc"><thead><tr><th>Method</th><th>Endpoint</th><th>Description</th></tr></thead><tbody><tr><td>POST</td><td>`/api/v1/admin/interests/categories`</td><td>Create category</td></tr><tr><td>GET</td><td>`/api/v1/admin/interests/categories`</td><td>List all (with stats)</td></tr><tr><td>PUT</td><td>`/api/v1/admin/interests/categories/{id}`</td><td>Update category</td></tr><tr><td>DELETE</td><td>`/api/v1/admin/interests/categories/{id}`</td><td>Deactivate category</td></tr><tr><td>PUT</td><td>`/api/v1/admin/interests/categories/reorder`</td><td>Reorder categories</td></tr><tr><td>GET</td><td>`/api/v1/admin/interests/categories/{id}/analytics`</td><td>Get analytics</td></tr></tbody></table>

### Public Endpoints

<table id="bkmrk-method-endpoint-desc-1"><thead><tr><th>Method</th><th>Endpoint</th><th>Description</th></tr></thead><tbody><tr><td>GET</td><td>`/api/v1/interests/categories`</td><td>Get active categories</td></tr></tbody></table>

### User Endpoints (Requires Auth)

<table id="bkmrk-method-endpoint-desc-2"><thead><tr><th>Method</th><th>Endpoint</th><th>Description</th></tr></thead><tbody><tr><td>GET</td><td>`/api/v1/interests/me`</td><td>Get my interests</td></tr><tr><td>PUT</td><td>`/api/v1/interests/me`</td><td>Update all explicit</td></tr><tr><td>POST</td><td>`/api/v1/interests/me/{id}`</td><td>Add single interest</td></tr><tr><td>DELETE</td><td>`/api/v1/interests/me/{id}`</td><td>Remove single interest</td></tr><tr><td>POST</td><td>`/api/v1/interests/me/{id}/hide`</td><td>Hide category</td></tr><tr><td>DELETE</td><td>`/api/v1/interests/me/{id}/hide`</td><td>Unhide category</td></tr><tr><td>GET</td><td>`/api/v1/interests/me/hidden`</td><td>Get hidden list</td></tr></tbody></table>

### Internal (No Public API)

- Silent tracking triggered by user actions
- Score calculation and decay
- Feed personalization

---

## New Database Fields Needed

### AccountEntity

```java
// 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> roles;
private List<String> 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<String>) - 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:**

```json
{
  "sub": "usr_550e8400e29b41d4",
  "tokenType": "ACCESS",
  "iat": 1736605200,
  "exp": 1736608800
}

```

**Refresh Token Payload:**

```json
{
  "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:**

```json
{
  "userName": "newusername"
}

```

**Response (Success):**

```json
{
  "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:

```java
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 &amp; 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

<table id="bkmrk-device-status-last-a"><thead><tr><th>Device Status</th><th>Last Activity</th><th>Password Set?</th><th>Action</th></tr></thead><tbody><tr><td>New (unknown)</td><td>Never</td><td>Yes</td><td>OTP first → then password → trust device</td></tr><tr><td>New (unknown)</td><td>Never</td><td>No</td><td>OTP only → trust device</td></tr><tr><td>Known trusted</td><td>&lt; 30 days</td><td>Yes</td><td>Password only ✅</td></tr><tr><td>Known trusted</td><td>&lt; 30 days</td><td>No</td><td>Auto-login or OTP</td></tr><tr><td>Known trusted</td><td>&gt; 30 days</td><td>Yes</td><td>OTP first → then password → re-trust</td></tr><tr><td>Known trusted</td><td>&gt; 30 days</td><td>No</td><td>OTP → re-trust</td></tr><tr><td>Known untrusted</td><td>Any</td><td>Any</td><td>OTP required</td></tr><tr><td>Any (suspicious)</td><td>Any</td><td>Any</td><td>OTP required + security alert</td></tr></tbody></table>

### 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:**

```json
{
  "identifier": "alexvibes",
  "password": "MySecurePass123!",
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS",
    "appVersion": "1.2.0"
  }
}

```

**Response (Success - Trusted Device):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456",
  "trustDevice": true,
  "deviceInfo": {
    "deviceId": "xyz789-new-device",
    "deviceName": "Chrome on Windows",
    "deviceType": "WEB_BROWSER"
  }
}

```

**Response (Success):**

```json
{
  "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:**

```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456",
  "trustDevice": false
}

```

**Response:**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "password": "MySecurePass123!",
  "keepCurrentDevice": true
}

```

**Response:**

```json
{
  "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:**

```json
{
  "deviceName": "My Work Laptop"
}

```

**Response:**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "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:**

```json
{
  "action": "DISMISS",
  "feedback": "This was me logging in from a friend's device"
}

```

**Response:**

```json
{
  "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

```yaml
# 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

<table id="bkmrk-%23-step-applies-to-en"><thead><tr><th>\#</th><th>Step</th><th>Applies To</th><th>Endpoint</th></tr></thead><tbody><tr><td>0</td><td>OTP / OAuth entry</td><td>All</td><td>`/auth/start` + `/auth/verify` or `/auth/login/oauth`</td></tr><tr><td>1</td><td>Age &amp; name</td><td>All</td><td>`/auth/onboarding/age`</td></tr><tr><td>2</td><td>Username</td><td>All</td><td>`/auth/onboarding/username`</td></tr><tr><td>3</td><td>Contact verify (PHONE)</td><td>Email + OAuth only</td><td>`/auth/onboarding/contact/initiate` + `/contact/verify`</td></tr><tr><td>4</td><td>Interests</td><td>All</td><td>`/auth/onboarding/interests`</td></tr><tr><td>5</td><td>Profile</td><td>All</td><td>`/auth/onboarding/profile`</td></tr></tbody></table>

### Contact Verification Rule

<table id="bkmrk-signup-method-requir"><thead><tr><th>Signup Method</th><th>Required Contact</th><th>Reason</th></tr></thead><tbody><tr><td>Phone</td><td><s>Email</s> (optional, post-onboarding)</td><td>Phone = real identity, M-Pesa already linked</td></tr><tr><td>Email</td><td>Phone (blocking)</td><td>Needed for M-Pesa payments</td></tr><tr><td>Google / Apple</td><td>Phone (blocking)</td><td>OAuth gives email, phone still required for payments</td></tr></tbody></table>

### 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

<table id="bkmrk-token-expiry-purpose"><thead><tr><th>Token</th><th>Expiry</th><th>Purpose</th></tr></thead><tbody><tr><td>`onboardingToken`</td><td>7 days</td><td>Active onboarding flow, resets each step</td></tr><tr><td>`onboardingRefreshToken`</td><td>30 days</td><td>Safety net if token expires mid-flow</td></tr><tr><td>`accessToken`</td><td>Configured</td><td>Normal app access</td></tr><tr><td>`refreshToken`</td><td>Configured</td><td>Access token rotation</td></tr></tbody></table>

---

## 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

<table id="bkmrk-input-pattern-detect"><thead><tr><th>Input pattern</th><th>Detected as</th></tr></thead><tbody><tr><td>Starts with `+`</td><td>Phone</td></tr><tr><td>Contains `@` + domain</td><td>Email</td></tr><tr><td>Otherwise</td><td>Username</td></tr></tbody></table>

### 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

<table id="bkmrk-login-method-new-dev"><thead><tr><th>Login Method</th><th>New Device Action</th><th>Reason</th></tr></thead><tbody><tr><td>OTP</td><td>None</td><td>OTP = already verified</td></tr><tr><td>Password</td><td>Require OTP</td><td>Password could be stolen</td></tr><tr><td>OAuth</td><td>Require OTP</td><td>Token could be compromised</td></tr></tbody></table>

---

## 6. Risk Scoring

<table id="bkmrk-signal-low-risk-high"><thead><tr><th>Signal</th><th>Low Risk</th><th>High Risk</th></tr></thead><tbody><tr><td>Location</td><td>Same country</td><td>New country</td></tr><tr><td>Device</td><td>Similar OS</td><td>Different OS</td></tr><tr><td>IP</td><td>Normal</td><td>VPN / Proxy</td></tr><tr><td>Account</td><td>Active</td><td>Dormant</td></tr><tr><td>Time</td><td>Normal hours</td><td>Unusual hours</td></tr></tbody></table>

### 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 &amp; 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

<table id="bkmrk-path-steps-phone-otp"><thead><tr><th>Path</th><th>Steps</th></tr></thead><tbody><tr><td>Phone</td><td>OTP → Age → Username → Interests → Profile → Home</td></tr><tr><td>Email</td><td>OTP → Age → Username → **Verify Phone** → Interests → Profile → Home</td></tr><tr><td>OAuth</td><td>Authorize → Age → Username → **Verify Phone** → Interests → Profile → Home</td></tr></tbody></table>

### Login

<table id="bkmrk-has-password%3F-option"><thead><tr><th>Has Password?</th><th>Options</th></tr></thead><tbody><tr><td>No</td><td>OTP only</td></tr><tr><td>Yes</td><td>Choose: OTP or Password</td></tr></tbody></table>

### Device Verification

<table id="bkmrk-login-method-new-dev-1"><thead><tr><th>Login Method</th><th>New Device Action</th></tr></thead><tbody><tr><td>OTP</td><td>None (OTP = verification)</td></tr><tr><td>Password</td><td>OTP required</td></tr><tr><td>OAuth</td><td>OTP required</td></tr></tbody></table>

### Post-Onboarding Nudges (Phone users)

<table id="bkmrk-feature-requires-ema"><thead><tr><th>Feature</th><th>Requires Email?</th><th>Behaviour</th></tr></thead><tbody><tr><td>Browse feed</td><td>No</td><td>Free</td></tr><tr><td>Buy ticket</td><td>No</td><td>Free</td></tr><tr><td>Receive ticket via email</td><td>Yes</td><td>Nudge to add email</td></tr><tr><td>Account recovery</td><td>Yes</td><td>Nudge to add email</td></tr></tbody></table>

# NextGate Authentication Specification v1.0 (DEPRECATED)

## 1. Overview

**Approach:** Passwordless-first, device-trusted, risk-aware authentication.

<table id="bkmrk-principle-implementa"><thead><tr><th>Principle</th><th>Implementation</th></tr></thead><tbody><tr><td>Passwordless default</td><td>OTP-based, password optional (add later in settings)</td></tr><tr><td>Device trust</td><td>Hardware-bound keys (mobile), fingerprint (web)</td></tr><tr><td>Risk-based</td><td>Dynamic verification based on risk score</td></tr><tr><td>Age-gated</td><td>18+ full access, tiered restrictions below</td></tr></tbody></table>

---

## 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

<table id="bkmrk-has-password%3F-login-"><thead><tr><th>Has Password?</th><th>Login Options</th></tr></thead><tbody><tr><td>No</td><td>OTP only (passwordless)</td></tr><tr><td>Yes</td><td>Choose: OTP or Password</td></tr></tbody></table>

<table id="bkmrk-login-method-new-dev"><thead><tr><th>Login Method</th><th>New Device Handling</th></tr></thead><tbody><tr><td>OTP</td><td>None needed — OTP is verification</td></tr><tr><td>Password</td><td>OTP required on new device</td></tr><tr><td>OAuth</td><td>OTP required on new device</td></tr></tbody></table>

---

## 5. Device Trust

### Platform Strategy

<table id="bkmrk-platform-method-trus"><thead><tr><th>Platform</th><th>Method</th><th>Trust Level</th></tr></thead><tbody><tr><td>iOS</td><td>Secure Enclave key pair</td><td>⭐⭐⭐⭐⭐ High</td></tr><tr><td>Android</td><td>StrongBox/TEE Keystore</td><td>⭐⭐⭐⭐⭐ High</td></tr><tr><td>Web</td><td>Fingerprint + session key</td><td>⭐⭐⭐ Medium</td></tr></tbody></table>

### 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 &amp; Weights

<table id="bkmrk-signal-low-%280%29-mediu"><thead><tr><th>Signal</th><th>Low (0)</th><th>Medium</th><th>High</th></tr></thead><tbody><tr><td>Location</td><td>Same city</td><td>Same country (+10)</td><td>New country (+25)</td></tr><tr><td>Device</td><td>Known (0)</td><td>Similar OS (+10)</td><td>New OS (+20)</td></tr><tr><td>IP</td><td>Normal ISP (0)</td><td>Different ISP (+10)</td><td>VPN/TOR (+30)</td></tr><tr><td>Time</td><td>Normal hours (0)</td><td>Unusual (+10)</td><td>2-5 AM (+15)</td></tr><tr><td>Failed attempts</td><td>None (0)</td><td>1-2 (+10)</td><td>3+ (+25)</td></tr><tr><td>Velocity</td><td>Normal (0)</td><td>Multiple (+15)</td><td>Rapid (+30)</td></tr><tr><td>Device signature</td><td>Valid (-20)</td><td>Missing (+15)</td><td>Invalid (+40)</td></tr></tbody></table>

### Thresholds &amp; Actions

<table id="bkmrk-score-risk-level-act"><thead><tr><th>Score</th><th>Risk Level</th><th>Action</th></tr></thead><tbody><tr><td>0-30</td><td>🟢 Low</td><td>Allow</td></tr><tr><td>31-60</td><td>🟡 Medium</td><td>Soft verify (email link)</td></tr><tr><td>61-85</td><td>🔴 High</td><td>Phone OTP required</td></tr><tr><td>86-100</td><td>⛔ Critical</td><td>Block + alert user</td></tr></tbody></table>

### 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

<table id="bkmrk-age-access-level-18%2B"><thead><tr><th>Age</th><th>Access Level</th></tr></thead><tbody><tr><td>18+</td><td>Full access</td></tr><tr><td>13-17</td><td>Restricted (no purchases, filtered content)</td></tr><tr><td>Under 13</td><td>Blocked (COPPA)</td></tr></tbody></table>

### 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)

<table id="bkmrk-account-age-allowed-"><thead><tr><th>Account Age</th><th>Allowed Changes</th></tr></thead><tbody><tr><td>Day 0 (today)</td><td>5 changes</td></tr><tr><td>1-30 days</td><td>1 per month</td></tr><tr><td>1-12 months</td><td>1 per month</td></tr><tr><td>12+ months</td><td>1 per year</td></tr><tr><td>SYSTEM accounts</td><td>Never</td></tr></tbody></table>

### Account Types

<table id="bkmrk-type-examples-userna"><thead><tr><th>Type</th><th>Examples</th><th>Username Change</th></tr></thead><tbody><tr><td>NORMAL</td><td>Regular users</td><td>Limited (above)</td></tr><tr><td>SYSTEM</td><td>@nextgate, @admin, @support</td><td>Never</td></tr><tr><td>VERIFIED</td><td>@nike, @cocacola</td><td>Requires approval</td></tr></tbody></table>

---

## 9. Session Management

### Sign Out Options

<table id="bkmrk-action-what-it-does-"><thead><tr><th>Action</th><th>What It Does</th><th>Requires</th></tr></thead><tbody><tr><td>Sign out</td><td>Current device only</td><td>Nothing</td></tr><tr><td>Sign out others</td><td>All except current</td><td>OTP/Password</td></tr><tr><td>Sign out all</td><td>Everything</td><td>OTP/Password</td></tr></tbody></table>

### 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

<table id="bkmrk-endpoint-purpose-pos"><thead><tr><th>Endpoint</th><th>Purpose</th></tr></thead><tbody><tr><td>`POST /auth/signup/initiate`</td><td>Start signup (phone/email)</td></tr><tr><td>`POST /auth/signup/verify-otp`</td><td>Verify OTP</td></tr><tr><td>`POST /auth/signup/age`</td><td>Submit birthdate</td></tr><tr><td>`POST /auth/signup/username`</td><td>Set username</td></tr><tr><td>`POST /auth/signup/interests`</td><td>Select interests</td></tr><tr><td>`POST /auth/signup/profile`</td><td>Complete profile (optional)</td></tr></tbody></table>

### Auth - Login

<table id="bkmrk-endpoint-purpose-pos-1"><thead><tr><th>Endpoint</th><th>Purpose</th></tr></thead><tbody><tr><td>`POST /auth/login/initiate`</td><td>Start login</td></tr><tr><td>`POST /auth/login/otp`</td><td>Login with OTP</td></tr><tr><td>`POST /auth/login/password`</td><td>Login with password</td></tr><tr><td>`GET /auth/challenge`</td><td>Get nonce for device signing</td></tr></tbody></table>

### Auth - Device

<table id="bkmrk-endpoint-purpose-pos-2"><thead><tr><th>Endpoint</th><th>Purpose</th></tr></thead><tbody><tr><td>`POST /auth/device/register`</td><td>Register device (public key)</td></tr><tr><td>`POST /auth/device/verify`</td><td>Verify new device OTP</td></tr><tr><td>`GET /auth/devices`</td><td>List trusted devices</td></tr><tr><td>`DELETE /auth/devices/{id}`</td><td>Revoke device</td></tr></tbody></table>

### Auth - Session

<table id="bkmrk-endpoint-purpose-get"><thead><tr><th>Endpoint</th><th>Purpose</th></tr></thead><tbody><tr><td>`GET /auth/sessions`</td><td>List active sessions</td></tr><tr><td>`POST /auth/sign-out`</td><td>Current device</td></tr><tr><td>`POST /auth/sign-out-others`</td><td>All except current</td></tr><tr><td>`POST /auth/sign-out-all`</td><td>Everything</td></tr></tbody></table>

---

## 12. Database Entities

### New Entities

<table id="bkmrk-entity-purpose-devic"><thead><tr><th>Entity</th><th>Purpose</th></tr></thead><tbody><tr><td>`DeviceKey`</td><td>Hardware-bound public keys</td></tr><tr><td>`UserSession`</td><td>Active sessions</td></tr><tr><td>`LoginAttempt`</td><td>Risk scoring data</td></tr><tr><td>`BlockedUser`</td><td>Blocked identifiers/devices</td></tr><tr><td>`InterestCategory`</td><td>Admin-managed interests</td></tr><tr><td>`UserInterest`</td><td>User selections</td></tr><tr><td>`UsernameChangeHistory`</td><td>Track changes</td></tr></tbody></table>

### AccountEntity Changes

<table id="bkmrk-field-change-passwor"><thead><tr><th>Field</th><th>Change</th></tr></thead><tbody><tr><td>`password`</td><td>Make nullable</td></tr><tr><td>`birthDate`</td><td>Add</td></tr><tr><td>`displayName`</td><td>Add</td></tr><tr><td>`accountType`</td><td>Add (NORMAL, SYSTEM, VERIFIED)</td></tr><tr><td>`accountTier`</td><td>Add (FULL, RESTRICTED, MINOR)</td></tr><tr><td>`authProvider`</td><td>Add (PHONE, EMAIL, GOOGLE, APPLE)</td></tr><tr><td>`onboardingStep`</td><td>Add</td></tr><tr><td>`usernameLastChangedAt`</td><td>Add</td></tr><tr><td>`usernameChangeCount`</td><td>Add</td></tr></tbody></table>

---

## 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

<table id="bkmrk-login-method-known-d"><thead><tr><th>Login Method</th><th>Known Device</th><th>New Device</th></tr></thead><tbody><tr><td>OTP</td><td>→ Home</td><td>→ Home (OTP is verification)</td></tr><tr><td>Password</td><td>→ Home</td><td>→ OTP required → Home</td></tr><tr><td>OAuth</td><td>→ Home</td><td>→ OTP required → Home</td></tr></tbody></table>

### Risk Score Quick Reference

```
0-30:   Allow
31-60:  Soft verify
61-85:  Phone OTP
86-100: Block + alert

```

---

## 15. Complete Device &amp; Auth Flow (Top to Bottom)

### When Does What Happen?

<table id="bkmrk-action-when-where-de"><thead><tr><th>Action</th><th>When</th><th>Where</th></tr></thead><tbody><tr><td>Device Registration</td><td>First app launch (before any auth)</td><td>Mobile only</td></tr><tr><td>Web Session Init</td><td>First visit (before any auth)</td><td>Web only</td></tr><tr><td>Risk Scoring</td><td>After credentials verified, before home</td><td>Login only</td></tr><tr><td>Device Verification OTP</td><td>After risk score (if needed)</td><td>Login only (new device + password/OAuth)</td></tr></tbody></table>

### 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

<table id="bkmrk-scenario-challenge%3F-"><thead><tr><th>Scenario</th><th>Challenge?</th><th>Signature Validated?</th></tr></thead><tbody><tr><td>**Signup**</td><td>❌ No</td><td>❌ No (device not registered yet)</td></tr><tr><td>**Login - Known device (mobile)**</td><td>✅ Yes</td><td>✅ Yes (must match)</td></tr><tr><td>**Login - Known device (web)**</td><td>✅ Yes</td><td>🟡 Fingerprint checked</td></tr><tr><td>**Login - Unknown device**</td><td>✅ Yes</td><td>❌ No pubkey stored yet</td></tr></tbody></table>

### 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

<table id="bkmrk-step-signup-login-de"><thead><tr><th>Step</th><th>Signup</th><th>Login</th></tr></thead><tbody><tr><td>Device key generation</td><td>✅ Before auth (app launch)</td><td>✅ Before auth (app launch)</td></tr><tr><td>OTP verification</td><td>✅ To verify phone/email</td><td>✅ As login method OR device verify</td></tr><tr><td>Age gate</td><td>✅ After OTP</td><td>❌ Not needed</td></tr><tr><td>Username</td><td>✅ Required</td><td>❌ Not needed</td></tr><tr><td>Interests</td><td>✅ Required</td><td>❌ Not needed</td></tr><tr><td>Risk scoring</td><td>❌ Not needed (new account)</td><td>✅ After credentials verified</td></tr><tr><td>Device verification OTP</td><td>❌ Not needed (first device)</td><td>✅ If new device + high risk</td></tr><tr><td>Device registration</td><td>✅ End of onboarding</td><td>✅ After device verification</td></tr></tbody></table>

### Risk Scoring: When &amp; 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

<table id="bkmrk-action-what-it-does--1"><thead><tr><th>Action</th><th>What It Does</th><th>Requires</th></tr></thead><tbody><tr><td>**Sign out**</td><td>End current session</td><td>Nothing</td></tr><tr><td>**Sign out other devices**</td><td>End all except current</td><td>OTP or Password</td></tr><tr><td>**Sign out all devices**</td><td>End everything</td><td>OTP or Password</td></tr></tbody></table>

### 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

<table id="bkmrk-endpoint-purpose-pos-3"><thead><tr><th>Endpoint</th><th>Purpose</th></tr></thead><tbody><tr><td>`POST /auth/sign-out`</td><td>Current device</td></tr><tr><td>`POST /auth/sign-out-others`</td><td>All except current</td></tr><tr><td>`POST /auth/sign-out-all`</td><td>Everything</td></tr><tr><td>`DELETE /auth/sessions/{id}`</td><td>Specific session</td></tr></tbody></table>

---

## 18. Industry Comparison

### NextGate vs Major Platforms

<table id="bkmrk-feature-nextgate-ins"><thead><tr><th>Feature</th><th>NextGate</th><th>Instagram</th><th>Twitter/X</th><th>WhatsApp</th><th>Banking Apps</th></tr></thead><tbody><tr><td>Passwordless default</td><td>✅ Yes</td><td>❌ No</td><td>❌ No</td><td>✅ Yes</td><td>🟡 Some</td></tr><tr><td>Hardware-bound keys</td><td>✅ Yes</td><td>❌ No</td><td>❌ No</td><td>❌ No</td><td>✅ Yes</td></tr><tr><td>Risk-based auth</td><td>✅ Yes</td><td>🟡 Basic</td><td>🟡 Basic</td><td>❌ No</td><td>✅ Yes</td></tr><tr><td>Device trust</td><td>✅ Advanced</td><td>🟡 Basic</td><td>🟡 Basic</td><td>🟡 Basic</td><td>✅ Advanced</td></tr><tr><td>Impossible travel detection</td><td>✅ Yes</td><td>🟡 Limited</td><td>🟡 Limited</td><td>❌ No</td><td>✅ Yes</td></tr><tr><td>Session management</td><td>✅ Full</td><td>✅ Full</td><td>✅ Full</td><td>🟡 Limited</td><td>✅ Full</td></tr><tr><td>Age verification</td><td>✅ Tiered</td><td>🟡 Basic</td><td>🟡 Basic</td><td>❌ No</td><td>✅ Yes</td></tr><tr><td>Username change limits</td><td>✅ Smart</td><td>🟡 14 days</td><td>🟡 Limited</td><td>❌ N/A</td><td>❌ N/A</td></tr><tr><td>2FA options</td><td>✅ OTP</td><td>✅ OTP/App</td><td>💰 Paid</td><td>❌ No</td><td>✅ Multiple</td></tr></tbody></table>

### 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

<table id="bkmrk-over-advantage-insta"><thead><tr><th>Over</th><th>Advantage</th></tr></thead><tbody><tr><td>Instagram/Twitter</td><td>Hardware-bound device keys, passwordless default</td></tr><tr><td>WhatsApp</td><td>Multi-identifier login, risk scoring, age gates</td></tr><tr><td>Basic apps</td><td>Challenge-response auth, impossible travel detection</td></tr></tbody></table>

### Future Improvements (v2)

<table id="bkmrk-feature-impact-effor"><thead><tr><th>Feature</th><th>Impact</th><th>Effort</th></tr></thead><tbody><tr><td>Passkeys/WebAuthn</td><td>+0.5 rating</td><td>Medium</td></tr><tr><td>Backup codes</td><td>+0.2 rating</td><td>Low</td></tr><tr><td>Breach monitoring</td><td>+0.2 rating</td><td>Low</td></tr><tr><td>ML anomaly detection</td><td>+0.3 rating</td><td>High</td></tr></tbody></table>

### 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

---

[![pona_auth_flow_diagram.jpg](https://doc-hub.qbitspark.com/uploads/images/gallery/2026-04/scaled-1680-/pona-auth-flow-diagram.jpg)](https://doc-hub.qbitspark.com/uploads/images/gallery/2026-04/pona-auth-flow-diagram.jpg)

## 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

```json
{
  "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

```json
{
  "success": true,
  "message": "Human readable message",
  "action": "NEXT_ACTION_OR_NULL",
  "data": { }
}
```

### Error — HTTP 422

```json
{
  "success": false,
  "message": "Human readable message",
  "action": "NEXT_ACTION_CODE",
  "context": "what_user_was_trying_to_do",
  "data": { }
}
```

---

## Response Examples

### /auth/check — New User
```json
{
  "success": true,
  "message": "Phone number not registered",
  "action": "REGISTER",
  "data": { "exists": false, "checkToken": null }
}
```

### /auth/check — Existing User Ready
```json
{
  "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
```json
{
  "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
```json
{
  "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
```json
{
  "success": true,
  "message": "Sending code to your phone",
  "action": "PROCEED_TO_OTP",
  "data": {
    "channels": [
      { "type": "PHONE", "masked": "••• ••• ••78", "isPrimary": true }
    ]
  }
}
```

### /auth/start — OTP Sent
```json
{
  "success": true,
  "message": "Verification code sent",
  "action": null,
  "data": {
    "tempToken": "eyJ...",
    "maskedDestination": "••• ••• ••78",
    "channel": "PHONE",
    "expiresInSeconds": 120,
    "resendAvailableAfterSeconds": 60
  }
}
```

### /auth/verify — Primary Incomplete
```json
{
  "success": true,
  "message": "Phone verified. Let us set up your account.",
  "action": "COLLECT_PRIMARY",
  "data": {
    "onboardingToken": "eyJ...",
    "nextStep": "name"
  }
}
```

### /auth/verify — Primary Already Complete
```json
{
  "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
```json
{
  "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
```json
{
  "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
```json
{
  "success": false,
  "message": "Incorrect OTP code",
  "action": "RETRY_OTP",
  "context": "otp_verify",
  "data": { "attemptsRemaining": 2 }
}
```

### OTP Expired
```json
{
  "success": false,
  "message": "OTP has expired",
  "action": "RESEND_OTP",
  "context": "otp_expired",
  "data": { "resendAvailable": true, "resendCooldownSeconds": 0 }
}
```

### Rate Limited
```json
{
  "success": false,
  "message": "Too many attempts. Please wait.",
  "action": "WAIT",
  "context": "rate_limited",
  "data": { "retryAfterSeconds": 120 }
}
```

### Wrong Auth Method
```json
{
  "success": false,
  "message": "This account does not use password login",
  "action": "USE_OTP",
  "context": "password_login",
  "data": { "availableMethods": ["passwordless", "google"] }
}
```

### Secondary Gate — Multiple Missing
```json
{
  "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
```json
{
  "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
```json
{
  "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
```json
{
  "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 <accessToken>
  { "username": "joshsakweli" }
          │
          ▼
  Username saved
  New accessToken issued
  action: COLLECT_EMAIL
  stepsRemaining: 1
  ............................................
  Frontend: "Step 2 of 2 — Add email"

  POST /onboarding/email/initiate
  Bearer <accessToken>
  { "email": "josh@qbitspark.com" }
          │
          ▼
  OTP sent to email
  tempToken returned
  nextAction: VERIFY_EMAIL
          │
          ▼
  POST /onboarding/email/verify
  Bearer <accessToken>
  { "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

```json
{
  "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](https://doc-hub.qbitspark.com/books/authentication-nexgate-service1/page/nextgate-pona-auth-flow-v3-progressive-onboarding-native-access#bkmrk-%2Fauth%2Fcheck-%E2%80%94-existi)

---

## 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.

[![pona_auth_flow_diagram.jpg](https://doc-hub.qbitspark.com/uploads/images/gallery/2026-04/scaled-1680-/pona-auth-flow-diagram.jpg)](https://doc-hub.qbitspark.com/uploads/images/gallery/2026-04/pona-auth-flow-diagram.jpg)

### 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
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Human-readable message",
  "action": "ACTION_CODE",
  "action_time": "2026-04-03T10:30:45",
  "data": {}
}
```

### Error response
```json
{
  "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:
```json
{ "channel": "SMS" }
```

Send via WhatsApp only:
```json
{ "channel": "WHATSAPP" }
```

Send via both SMS and WhatsApp at the same time:
```json
{ "channel": "SMS_AND_WHATSAPP" }
```

Send via email (login only, verified email required):
```json
{ "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.).

```json
{
  "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

- <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> — Read only
- <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> — Create / action
- <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> — 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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/check`

**Access Level**: 🌐 Public

**Authentication**: None

**Request**:
```json
{
  "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**:
```json
{
  "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**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/passwordless/channels`

**Access Level**: 🌐 Public

**Authentication**: None

> This endpoint does NOT consume the checkToken.

**Request**:
```json
{
  "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**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/passwordless-start`

**Access Level**: 🌐 Public

**Authentication**: None

**Request — SMS only**:
```json
{
  "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
  "channel": "SMS",
  "deviceId": "android-uuid-abc123"
}
```

**Request — WhatsApp only**:
```json
{
  "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
  "channel": "WHATSAPP",
  "deviceId": "android-uuid-abc123"
}
```

**Request — Both SMS and WhatsApp simultaneously**:
```json
{
  "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
  "channel": "SMS_AND_WHATSAPP",
  "deviceId": "android-uuid-abc123"
}
```

**Request — Email (login only, verified email required)**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/verify-otp`

**Access Level**: 🌐 Public

**Authentication**: None

**Request**:
```json
{
  "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**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/resend-otp`

**Access Level**: 🌐 Public

**Authentication**: None

**Request**:
```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiJ9..."
}
```

**Response**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/onboarding/primary`

**Access Level**: 🌐 Public

**Authentication**: None (uses `onboardingToken` in body)

**Request**:
```json
{
  "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**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/login/password`

**Access Level**: 🌐 Public

**Authentication**: None

**Request**:
```json
{
  "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**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/login/oauth`

**Access Level**: 🌐 Public

**Authentication**: None

**Request**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/password/forgot/initiate`

**Access Level**: 🌐 Public

**Authentication**: None

**Request**:
```json
{
  "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
  "deviceId": "android-uuid-abc123"
}
```

**Response**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/password/forgot/verify-otp`

**Access Level**: 🌐 Public

**Authentication**: None

**Request**:
```json
{
  "tempToken": "eyJhbGciOiJIUzI1NiJ9...",
  "otp": "482910"
}
```

**Response**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/password/forgot/reset`

**Access Level**: 🌐 Public

**Authentication**: None

**Request**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/token/refresh`

**Access Level**: 🌐 Public

**Authentication**: None

**Request**:
```json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}
```

**Response**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/token/revoke`

**Access Level**: 🌐 Public

**Authentication**: None

**Request**:
```json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}
```

**Response**:
```json
{
  "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
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-09-23T10:30:45",
  "action": "COLLECT_EMAIL",
  "data": {}
}
```

### Error Response Structure
```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/username/suggestions`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |

**Success Response JSON Sample**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/username`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:
```json
{
  "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**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/bio`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/interests`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/profile-pic`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/email/custom/initiate`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/email/custom/verify`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:
```json
{
  "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**:
```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/email/google`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:
```json
{
  "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**:
```json
{
  "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 |