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:


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

Token Purpose Expiry Storage
tempToken OTP verification, flow state 10 min Memory only
onboardingToken Onboarding step progression 30 min Memory only
onboardingRefreshToken Refresh onboarding token 24 hours Secure storage
accessToken API authentication 1 hour Secure storage
refreshToken Get new access tokens 30 days Secure storage
deviceVerificationToken New device verification 10 min Memory only

Onboarding Steps Explained

Step Enum Value What Happens Metadata Provided
1 INITIATED Account created, OTP sent -
2 OTP_VERIFIED OTP confirmed, ready for age firstName, lastName, profilePicture (from OAuth)
3 AGE_VERIFIED Age confirmed, names saved suggestedUsernames (5 smart suggestions)
4 USERNAME_SET Username chosen -
5 INTERESTS_SELECTED Interests saved firstName, lastName, username, suggestedDisplayName, profilePicture
6 COMPLETED Profile complete, tokens issued -

Device Fingerprinting Guide

Why Device Fingerprinting?

Device fingerprinting creates a unique identifier for each device/browser combination. This enables:

Implementation for Frontend

npm install @fingerprintjs/fingerprintjs

React Implementation

// hooks/useDeviceFingerprint.js
import { useState, useEffect } from 'react';
import FingerprintJS from '@fingerprintjs/fingerprintjs';

export const useDeviceFingerprint = () => {
  const [fingerprint, setFingerprint] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const generateFingerprint = async () => {
      try {
        const fp = await FingerprintJS.load();
        const result = await fp.get();
        
        setFingerprint({
          deviceId: result.visitorId,
          confidence: result.confidence.score,
          components: {
            platform: result.components.platform?.value,
            timezone: result.components.timezone?.value,
            language: result.components.languages?.value?.[0],
            screenResolution: result.components.screenResolution?.value,
            colorDepth: result.components.colorDepth?.value,
          }
        });
      } catch (error) {
        console.error('Fingerprint generation failed:', error);
        // Fallback: Generate a random UUID and store in localStorage
        let fallbackId = localStorage.getItem('ng_device_id');
        if (!fallbackId) {
          fallbackId = crypto.randomUUID();
          localStorage.setItem('ng_device_id', fallbackId);
        }
        setFingerprint({ deviceId: fallbackId, confidence: 0.5 });
      } finally {
        setLoading(false);
      }
    };

    generateFingerprint();
  }, []);

  return { fingerprint, loading };
};

Usage in Authentication

// components/LoginForm.jsx
import { useDeviceFingerprint } from '../hooks/useDeviceFingerprint';

const LoginForm = () => {
  const { fingerprint, loading } = useDeviceFingerprint();

  const handleStartAuth = async (identifier) => {
    if (loading || !fingerprint) {
      console.warn('Device fingerprint not ready');
      return;
    }

    const response = await fetch('/api/v1/auth/start', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        identifier: identifier,
        deviceId: fingerprint.deviceId,
        deviceName: getDeviceName(),
        platform: getPlatform(),
      }),
    });

    const data = await response.json();
    // Handle response...
  };

  return (/* Your form JSX */);
};

// Helper: Generate human-readable device name
const getDeviceName = () => {
  const ua = navigator.userAgent;
  let browser = 'Unknown Browser';
  let os = 'Unknown OS';

  if (ua.includes('Chrome')) browser = 'Chrome';
  else if (ua.includes('Firefox')) browser = 'Firefox';
  else if (ua.includes('Safari')) browser = 'Safari';
  else if (ua.includes('Edge')) browser = 'Edge';

  if (ua.includes('Windows')) os = 'Windows';
  else if (ua.includes('Mac')) os = 'macOS';
  else if (ua.includes('Linux')) os = 'Linux';
  else if (ua.includes('Android')) os = 'Android';
  else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS';

  return `${browser} on ${os}`;
};

// Helper: Get platform type
const getPlatform = () => {
  const ua = navigator.userAgent;
  if (ua.includes('Android')) return 'ANDROID';
  if (ua.includes('iPhone') || ua.includes('iPad')) return 'IOS';
  return 'WEB';
};

Alternative: Custom Fingerprint (No Library)

// utils/customFingerprint.js
export const generateCustomFingerprint = async () => {
  const components = [];

  // Screen properties
  components.push(window.screen.width);
  components.push(window.screen.height);
  components.push(window.screen.colorDepth);
  components.push(window.devicePixelRatio);

  // Timezone & Language
  components.push(Intl.DateTimeFormat().resolvedOptions().timeZone);
  components.push(navigator.language);
  components.push(navigator.platform);
  components.push(navigator.hardwareConcurrency || 'unknown');

  // Canvas fingerprint
  try {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    ctx.textBaseline = 'top';
    ctx.font = '14px Arial';
    ctx.fillText('NextGate Device FP', 2, 2);
    components.push(canvas.toDataURL());
  } catch (e) {
    components.push('canvas-blocked');
  }

  // WebGL renderer
  try {
    const canvas = document.createElement('canvas');
    const gl = canvas.getContext('webgl');
    const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
    if (debugInfo) {
      components.push(gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL));
    }
  } catch (e) {
    components.push('webgl-blocked');
  }

  // Create hash
  const fingerprint = components.join('|||');
  const encoder = new TextEncoder();
  const data = encoder.encode(fingerprint);
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
};

Required Headers for All Auth Requests

const authHeaders = {
  'Content-Type': 'application/json',
  'X-Device-Id': fingerprint.deviceId,
  'X-Device-Name': getDeviceName(),
  'X-Platform': getPlatform(),
};

// For authenticated requests:
authHeaders['Authorization'] = `Bearer ${accessToken}`;

// For session management:
authHeaders['X-Session-Id'] = sessionId;
┌─────────────────────────────────────────────────────────────────────────────┐
│                         COMPLETE ONBOARDING FLOW                             │
└─────────────────────────────────────────────────────────────────────────────┘

STEP 0: ENTRY POINT
═══════════════════
     ┌────────────────────────────────────────────────────────────┐
     │                    Two Entry Points                         │
     │                                                             │
     │   A) Phone/Email Login          B) OAuth (Google/Apple)    │
     │      POST /auth/start              POST /auth/login/oauth  │
     │           │                              │                 │
     │           ▼                              ▼                 │
     │      OTP Sent to User            Google Validates User     │
     │           │                              │                 │
     │           ▼                              │                 │
     │      POST /auth/verify                   │                 │
     │           │                              │                 │
     │           └──────────────┬───────────────┘                 │
     │                          │                                 │
     │                          ▼                                 │
     │              Response contains:                            │
     │              • onboardingToken                             │
     │              • currentStep: INITIATED                      │
     │              • nextStep: AGE_VERIFIED                      │
     │              • nextStepMetadata: {                         │
     │                  firstName: "John",    // from OAuth       │
     │                  lastName: "Doe",      // from OAuth       │
     │                  profilePicture: "..." // from OAuth       │
     │                  hasOAuthData: true                        │
     │                }                                           │
     └────────────────────────────────────────────────────────────┘
                                │
                                ▼
STEP 1: AGE & NAME VERIFICATION
════════════════════════════════
POST /auth/onboarding/age

     ┌────────────────────────────────────────────────────────────┐
     │  UI:                                                        │
     │  ┌──────────────────────────────────────────────────────┐  │
     │  │  📅 Let's verify your age                            │  │
     │  │                                                      │  │
     │  │  First Name: [John____________] <- Pre-filled OAuth  │  │
     │  │  Last Name:  [Doe_____________] <- Pre-filled OAuth  │  │
     │  │  Birth Date: [____/____/______]                      │  │
     │  │                                                      │  │
     │  │  [Continue →]                                        │  │
     │  └──────────────────────────────────────────────────────┘  │
     │                                                             │
     │  REQUEST:                                                   │
     │  {                                                          │
     │    "onboardingToken": "eyJ...",                             │
     │    "firstName": "John",                                     │
     │    "lastName": "Doe",                                       │
     │    "birthDate": "1999-05-15"                                │
     │  }                                                          │
     │                                                             │
     │  RESPONSE:                                                  │
     │  {                                                          │
     │    "onboardingToken": "eyJ...",                             │
     │    "accountTier": "FULL",                                   │
     │    "age": 25,                                               │
     │    "currentStep": "AGE_VERIFIED",                           │
     │    "nextStep": "USERNAME_SET",                              │
     │    "nextStepMetadata": {                                    │
     │      "suggestedUsernames": [                                │
     │        "johndoe99", "john_doe_official", "jdoe_tz"         │
     │      ]                                                      │
     │    }                                                        │
     │  }                                                          │
     │                                                             │
     │  ⚠️  age < 13  -> blocked, account deleted                 │
     │  ⚠️  age 13-17 -> accountTier = RESTRICTED                 │
     └────────────────────────────────────────────────────────────┘
                                │
                                ▼
STEP 2: USERNAME SELECTION
══════════════════════════
POST /auth/onboarding/username
POST /auth/onboarding/check-username  (real-time availability)

     ┌────────────────────────────────────────────────────────────┐
     │  UI:                                                        │
     │  ┌──────────────────────────────────────────────────────┐  │
     │  │  👤 Choose your username                             │  │
     │  │                                                      │  │
     │  │  @[________________]                                 │  │
     │  │                                                      │  │
     │  │  Suggestions:                                        │  │
     │  │  [ johndoe99 ]  [ john_doe ]  [ jdoe_tz ]           │  │
     │  │  [ the_johndoe ]  [ johnd_pro ]                     │  │
     │  │                                                      │  │
     │  │  [Continue →]                                        │  │
     │  └──────────────────────────────────────────────────────┘  │
     │                                                             │
     │  RESPONSE:                                                  │
     │  {                                                          │
     │    "onboardingToken": "eyJ...",                             │
     │    "username": "johndoe99",                                 │
     │    "currentStep": "USERNAME_SET",                           │
     │    "nextStep": "CONTACT_VERIFIED",          <-- NEW        │
     │    "nextStepMetadata": {                                    │
     │      "contactVerification": {                               │
     │        "requiredContactType": "EMAIL",                      │
     │        "requiresInput": true,                               │
     │        "alreadyVerified": {                                 │
     │          "type": "PHONE",                                   │
     │          "maskedValue": "... ... ..50"                      │
     │        },                                                   │
     │        "reasons": [                                         │
     │          "TICKET_DELIVERY",                                 │
     │          "ACCOUNT_RECOVERY",                                │
     │          "ACCOUNT_SECURITY"                                 │
     │        ]                                                    │
     │      }                                                      │
     │    }                                                        │
     │  }                                                          │
     └────────────────────────────────────────────────────────────┘
                                │
                                ▼
STEP 3: CONTACT VERIFICATION                         <-- NEW STEP
═══════════════════════════════
POST /auth/onboarding/contact/initiate
POST /auth/onboarding/contact/verify
POST /auth/onboarding/contact/edit    (inline, no page nav)

     ┌────────────────────────────────────────────────────────────┐
     │  UI (single page, three inline states):                     │
     │                                                             │
     │  STATE A — Input (if requiresInput = true):                 │
     │  ┌──────────────────────────────────────────────────────┐  │
     │  │  📧 Add your email address                           │  │
     │  │                                                      │  │
     │  │  Your phone ... ...50 is already verified ✅         │  │
     │  │                                                      │  │
     │  │  We need your email for:                             │  │
     │  │  • Ticket delivery                                   │  │
     │  │  • Account recovery                                  │  │
     │  │  • Security alerts                                   │  │
     │  │                                                      │  │
     │  │  Email: [________________________]                   │  │
     │  │  [Send Code →]                                       │  │
     │  └──────────────────────────────────────────────────────┘  │
     │                                                             │
     │  INITIATE REQUEST:                                          │
     │  {                                                          │
     │    "onboardingToken": "eyJ...",                             │
     │    "contactValue": "john@gmail.com"                         │
     │  }                                                          │
     │                                                             │
     │  INITIATE RESPONSE:                                         │
     │  {                                                          │
     │    "tempToken": "eyJ...",                                   │
     │    "onboardingToken": "eyJ...",                             │
     │    "maskedValue": "j......@g.....com",                      │
     │    "contactType": "EMAIL",                                  │
     │    "expiresIn": 600                                         │
     │  }                                                          │
     │                                                             │
     │  STATE B — OTP Input:                                       │
     │  ┌──────────────────────────────────────────────────────┐  │
     │  │  Enter the code sent to j......@g.....com            │  │
     │  │                                                      │  │
     │  │  [_] [_] [_] [_] [_] [_]                            │  │
     │  │                                                      │  │
     │  │  [Edit email] · Resend (60s cooldown)                │  │
     │  └──────────────────────────────────────────────────────┘  │
     │                                                             │
     │  VERIFY REQUEST:                                            │
     │  {                                                          │
     │    "onboardingToken": "eyJ...",                             │
     │    "tempToken": "eyJ...",                                   │
     │    "otp": "847291"                                          │
     │  }                                                          │
     │                                                             │
     │  VERIFY RESPONSE:                                           │
     │  {                                                          │
     │    "onboardingToken": "eyJ...",                             │
     │    "currentStep": "CONTACT_VERIFIED",                       │
     │    "nextStep": "INTERESTS_SELECTED",                        │
     │    "verified": true                                         │
     │  }                                                          │
     │                                                             │
     │  STATE C — Edit (tapping [Edit email] transforms inline):   │
     │  ┌──────────────────────────────────────────────────────┐  │
     │  │  New email: [________________________]               │  │
     │  │  [Send new code →]       [Cancel]                    │  │
     │  └──────────────────────────────────────────────────────┘  │
     │                                                             │
     │  EDIT REQUEST:                                              │
     │  {                                                          │
     │    "onboardingToken": "eyJ...",                             │
     │    "tempToken": "eyJ...",                                   │
     │    "newContactValue": "other@gmail.com"                     │
     │  }                                                          │
     │  -> Returns new tempToken + new maskedValue                 │
     │  -> Previous OTP invalidated immediately                    │
     │                                                             │
     │  requiredContactType by signup method:                      │
     │  • Phone user   -> verify EMAIL                            │
     │  • Email user   -> verify PHONE                            │
     │  • Google/Apple -> verify PHONE (email via OAuth already)  │
     └────────────────────────────────────────────────────────────┘
                                │
                                ▼
STEP 4: INTERESTS SELECTION
═══════════════════════════
GET  /interests/categories/all
POST /auth/onboarding/interests

     ┌────────────────────────────────────────────────────────────┐
     │  UI:                                                        │
     │  ┌──────────────────────────────────────────────────────┐  │
     │  │  🎯 What are you interested in?  (select at least 3) │  │
     │  │                                                      │  │
     │  │  [🎵 Music] [⚽ Sports] [🎮 Gaming] [📱 Tech]        │  │
     │  │  [🎬 Movies] [📚 Books] [🍕 Food]  [✈️ Travel]      │  │
     │  │                           ... more                   │  │
     │  │                                                      │  │
     │  │  [Continue →]                                        │  │
     │  └──────────────────────────────────────────────────────┘  │
     │                                                             │
     │  RESPONSE:                                                  │
     │  {                                                          │
     │    "onboardingToken": "eyJ...",                             │
     │    "selectedInterests": ["Music", "Tech", "Travel"],        │
     │    "count": 3,                                              │
     │    "currentStep": "INTERESTS_SELECTED",                     │
     │    "nextStep": "PROFILE_COMPLETED",                         │
     │    "nextStepMetadata": {                                    │
     │      "firstName": "John",                                   │
     │      "lastName": "Doe",                                     │
     │      "username": "johndoe99",                               │
     │      "suggestedDisplayName": "John Doe",                    │
     │      "profilePicture": "https://..."                        │
     │    }                                                        │
     │  }                                                          │
     └────────────────────────────────────────────────────────────┘
                                │
                                ▼
STEP 5: PROFILE COMPLETION (FINAL)
══════════════════════════════════
POST /auth/onboarding/profile

     ┌────────────────────────────────────────────────────────────┐
     │  UI:                                                        │
     │  ┌──────────────────────────────────────────────────────┐  │
     │  │  ✨ Complete your profile                            │  │
     │  │                                                      │  │
     │  │       [ 📷 photo ] <- Pre-filled from OAuth          │  │
     │  │                       or upload new                  │  │
     │  │                                                      │  │
     │  │  Display Name: [John Doe________] <- Pre-filled      │  │
     │  │  @johndoe99  (locked here, change in settings)       │  │
     │  │                                                      │  │
     │  │  Bio: [________________________________]             │  │
     │  │                                                      │  │
     │  │  [🎉 Complete Setup]                                 │  │
     │  └──────────────────────────────────────────────────────┘  │
     │                                                             │
     │  HEADERS:                                                   │
     │  X-Device-Id:   "abc123fingerprint"                         │
     │  X-Device-Name: "Chrome on macOS"                           │
     │  X-Platform:    "WEB"                                       │
     │                                                             │
     │  REQUEST:                                                   │
     │  {                                                          │
     │    "onboardingToken": "eyJ...",                             │
     │    "displayName": "John Doe",                               │
     │    "bio": "Tech enthusiast | Based in Dar es Salaam",       │
     │    "profilePictureUrl": "https://storage.nextgate.com/..."  │
     │  }                                                          │
     │                                                             │
     │  RESPONSE:                                                  │
     │  {                                                          │
     │    "accessToken":  "eyJ...",                                │
     │    "refreshToken": "eyJ...",                                │
     │    "systemUsername": "su_uuid",                             │
     │    "username": "johndoe99",                                 │
     │    "displayName": "John Doe",                               │
     │    "currentStep": "COMPLETED",                              │
     │    "onboardingComplete": true,                              │
     │    "message": "Welcome to NextGate!"                        │
     │  }                                                          │
     └────────────────────────────────────────────────────────────┘
                                │
                                ▼
                    ┌───────────────────────┐
                    │   🎉 WELCOME!         │
                    │                       │
                    │   User is fully       │
                    │   authenticated and   │
                    │   can access the app  │
                    └───────────────────────┘

Handling Onboarding Resume

When a user returns with incomplete onboarding:

const handleOnboardingResponse = (response) => {
  const { currentStep, nextStep, nextStepMetadata, onboardingToken } = response.data;

  sessionStorage.setItem('onboardingToken', onboardingToken);

  switch (nextStep) {
    case 'AGE_VERIFIED':
      navigate('/onboarding/age', { state: { metadata: nextStepMetadata } });
      break;
    case 'USERNAME_SET':
      navigate('/onboarding/username', { state: { metadata: nextStepMetadata } });
      break;
    case 'INTERESTS_SELECTED':
      navigate('/onboarding/interests');
      break;
    case 'PROFILE_COMPLETED':
      navigate('/onboarding/profile', { state: { metadata: nextStepMetadata } });
      break;
    case 'COMPLETED':
      handleFullAuth(response.data);
      break;
  }
};

Standard Response Format

All API responses follow a consistent structure:

Success Response Structure:

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

Error Response Structure:

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

Standard Response Fields:

Field Type Description
success boolean true for successful operations, false for errors
httpStatus string HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.)
message string Human-readable message
action_time string ISO 8601 timestamp
data object/string Response payload or error details

API Endpoints

Authentication Initialization

1. Start Authentication

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

Endpoint: POST {base_url}/auth/start

Access Level: 🌐 Public

Authentication: None

Request JSON Sample:

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

Request Body Parameters:

Parameter Type Required Description Validation
identifier string Yes Phone (+255...), email, or @username Valid format
deviceId string Yes Device fingerprint Non-empty
deviceName string Yes Human-readable device name Non-empty
platform string Yes Device platform enum: IOS, ANDROID, WEB

Success Response JSON Sample (New User):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Authentication initiated",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "maskedIdentifier": "••• ••• ••78",
    "provider": "PHONE",
    "otpExpiresIn": 600,
    "isNewUser": true,
    "onboardingComplete": false,
    "currentStep": "INITIATED",
    "hasPassword": false,
    "requiresOtpChannelSelection": false,
    "message": "OTP sent to ••• ••• ••78"
  }
}

Success Response JSON Sample (Existing User with Password):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Authentication initiated",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "maskedIdentifier": "j••••••@g••••.com",
    "provider": "EMAIL",
    "otpExpiresIn": 600,
    "isNewUser": false,
    "onboardingComplete": true,
    "currentStep": "COMPLETED",
    "hasPassword": true,
    "requiresOtpChannelSelection": false,
    "message": "Choose login method: password or OTP"
  }
}

Success Response JSON Sample (Username - Channel Selection Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Authentication initiated",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "hasPassword": true,
    "requiresOtpChannelSelection": true,
    "availableChannels": [
      { "type": "EMAIL", "maskedValue": "j••••••@g••••.com", "isPrimary": true },
      { "type": "PHONE", "maskedValue": "••• ••• ••78", "isPrimary": false }
    ],
    "isNewUser": false,
    "onboardingComplete": true,
    "message": "Select where to receive OTP"
  }
}

Success Response Fields:

Field Description
tempToken Temporary token for next step
maskedIdentifier Masked phone/email for display
provider Identifier type: PHONE, EMAIL
otpExpiresIn OTP validity in seconds (600 = 10 minutes)
isNewUser Whether this is a new registration
onboardingComplete Whether user completed onboarding
currentStep Current onboarding step
hasPassword Whether user has set a password
requiresOtpChannelSelection If true, user must choose OTP channel
availableChannels List of available OTP channels

Error Responses:

Identifier Blocked (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "This phone is blocked",
  "action_time": "2025-01-24T10:30:45",
  "data": "This phone is blocked"
}

Account Not Found - Username (404):

{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Account not found",
  "action_time": "2025-01-24T10:30:45",
  "data": "Account not found"
}

2. Verify Authentication OTP

Purpose: Verify OTP and complete authentication or continue to onboarding

Endpoint: POST {base_url}/auth/verify

Access Level: 🌐 Public

Authentication: None

Request JSON Sample:

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

Request Body Parameters:

Parameter Type Required Description Validation
tempToken string Yes Token from /auth/start Valid JWT
otp string Yes 6-digit OTP code Exactly 6 digits
deviceId string No Device fingerprint -
deviceName string No Human-readable device name -
platform string No Device platform enum: IOS, ANDROID, WEB

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

{
  "success": true,
  "httpStatus": "OK",
  "message": "Verification successful",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "onboardingComplete": false,
    "currentStep": "OTP_VERIFIED",
    "nextStep": "AGE_VERIFIED",
    "nextStepMetadata": null,
    "message": "OTP verified. Let's set up your account."
  }
}

Success Response JSON Sample (OAuth User - Has Profile Data):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Verification successful",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "onboardingComplete": false,
    "currentStep": "OTP_VERIFIED",
    "nextStep": "AGE_VERIFIED",
    "nextStepMetadata": {
      "firstName": "John",
      "lastName": "Doe",
      "profilePicture": "https://lh3.googleusercontent.com/...",
      "hasOAuthData": true
    },
    "message": "OTP verified. Let's set up your account."
  }
}

Success Response JSON Sample (Existing User - Fully Authenticated):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Verification successful",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "systemUsername": "su_550e8400-e29b-41d4-a716-446655440000",
    "username": "johndoe99",
    "displayName": "John Doe",
    "accountTier": "FULL",
    "onboardingComplete": true,
    "currentStep": "COMPLETED",
    "message": "Login successful"
  }
}

Error Responses:

Invalid OTP (403):

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

Max Attempts Exceeded (403):

{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Maximum verification attempts exceeded",
  "action_time": "2025-01-24T10:30:45",
  "data": "Maximum verification attempts exceeded"
}

3. Send OTP to Channel

Purpose: Send OTP to a specific channel when user has multiple options

Endpoint: POST {base_url}/auth/send-otp-to-channel

Access Level: 🌐 Public

Authentication: None

Request JSON Sample:

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

Request Body Parameters:

Parameter Type Required Description Validation
tempToken string Yes Token from /auth/start Valid JWT
channel string Yes Preferred OTP channel enum: EMAIL, PHONE

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP sent",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "sentTo": "EMAIL",
    "maskedDestination": "j••••••@g••••.com",
    "expiresIn": 600,
    "message": "OTP sent to email"
  }
}

4. Login with Password

Purpose: Authenticate using password instead of OTP

Endpoint: POST {base_url}/auth/login/password

Access Level: 🌐 Public

Authentication: None

Request JSON Sample:

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

Success Response JSON Sample (Known Device):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "systemUsername": "su_550e8400-e29b-41d4-a716-446655440000",
    "username": "johndoe99",
    "displayName": "John Doe",
    "accountTier": "FULL",
    "newDevice": false,
    "requiresDeviceVerification": false,
    "message": "Login successful"
  }
}

Success Response JSON Sample (New Device - Verification Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "newDevice": true,
    "requiresDeviceVerification": true,
    "deviceVerificationToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "requiresChannelSelection": false,
    "otpSentTo": "••• ••• ••78",
    "message": "Device verification required. OTP sent."
  }
}

Error Responses:

Invalid Password (403):

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

5. OAuth Login (Google)

Purpose: Authenticate using Google OAuth2 authorization code flow

Endpoint: POST {base_url}/auth/login/oauth

Access Level: 🌐 Public

Authentication: None

Request JSON Sample:

{
  "provider": "GOOGLE",
  "code": "4/0AX4XfWh...",
  "redirectUri": "https://app.nextgate.com/auth/callback",
  "state": "random-state-string",
  "deviceId": "a1b2c3d4e5f6789",
  "deviceName": "Chrome on macOS",
  "platform": "WEB"
}

Request Body Parameters:

Parameter Type Required Description Validation
provider string Yes OAuth provider enum: GOOGLE, APPLE
code string Yes Authorization code Non-empty
redirectUri string Yes Redirect URI Must match OAuth config
state string No State for CSRF protection -
deviceId string No Device fingerprint -
deviceName string No Device name -
platform string No Device platform enum: IOS, ANDROID, WEB

Success Response JSON Sample (New User):

{
  "success": true,
  "httpStatus": "OK",
  "message": "OAuth login processed",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "newUser": true,
    "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "currentStep": "OTP_VERIFIED",
    "nextStep": "AGE_VERIFIED",
    "nextStepMetadata": {
      "firstName": "John",
      "lastName": "Doe",
      "profilePicture": "https://lh3.googleusercontent.com/a/...",
      "hasOAuthData": true
    },
    "message": "Complete your registration",
    "state": "random-state-string"
  }
}

Success Response JSON Sample (Existing User):

{
  "success": true,
  "httpStatus": "OK",
  "message": "OAuth login processed",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "newUser": false,
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "systemUsername": "su_550e8400-e29b-41d4-a716-446655440000",
    "username": "johndoe99",
    "displayName": "John Doe",
    "accountTier": "FULL",
    "message": "Login successful",
    "state": "random-state-string"
  }
}

6. Verify Device

Purpose: Verify a new device using OTP

Endpoint: POST {base_url}/auth/device/verify

Access Level: 🌐 Public

Authentication: None

Request JSON Sample:

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

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device verified",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "systemUsername": "su_550e8400-e29b-41d4-a716-446655440000",
    "username": "johndoe99",
    "displayName": "John Doe",
    "accountTier": "FULL",
    "message": "Login successful"
  }
}

7. Resend OTP

Purpose: Resend OTP code (with rate limiting)

Endpoint: POST {base_url}/auth/resend-otp

Access Level: 🌐 Public

Authentication: None

Request JSON Sample:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP resend processed",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "maskedDestination": "••• ••• ••78",
    "sentTo": "PHONE",
    "expiresIn": 600,
    "attemptsRemaining": 4,
    "cooldownSeconds": 0,
    "message": "OTP resent"
  }
}

Cooldown Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP resend processed",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "cooldownSeconds": 45,
    "attemptsRemaining": 3,
    "message": "Please wait 45 seconds before requesting again"
  }
}

Onboarding Endpoints


8. Set Age (Step 1)

Purpose: Verify user's age and save first/last name

Endpoint: POST {base_url}/auth/onboarding/age

Access Level: Public (requires valid onboarding token)

Request Body:

{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "firstName": "John",
  "lastName": "Doe",
  "birthDate": "1999-05-15"
}
Parameter Type Required Description Validation
onboardingToken string Yes Token from previous step Valid JWT
firstName string Yes User's first name 1-50 characters
lastName string Yes User's last name 1-50 characters
birthDate string Yes Date of birth ISO date (YYYY-MM-DD), must be in past

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Age verified",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "accountTier": "FULL",
    "age": 25,
    "restricted": false,
    "blocked": false,
    "currentStep": "AGE_VERIFIED",
    "nextStep": "USERNAME_SET",
    "nextStepMetadata": {
      "suggestedUsernames": [
        "johndoe99",
        "john_doe_official",
        "jdoe_tz",
        "the_johndoe",
        "johnd_pro"
      ]
    },
    "message": "Age verified successfully"
  }
}

Blocked Response (Under 13):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Age verified",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "accountTier": null,
    "age": 12,
    "restricted": true,
    "blocked": true,
    "currentStep": "AGE_VERIFIED",
    "nextStep": null,
    "message": "You must be at least 13 years old to use NextGate"
  }
}

If age < 13: account is blocked and deleted. If age 13-17: accountTier = RESTRICTED.


9. Check Username Availability

Purpose: Real-time username availability check

Endpoint: POST {base_url}/auth/onboarding/check-username

Access Level: Public

Request Body:

{
  "username": "johndoe99"
}

Response - Available:

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

Response - Not Available:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Username not available",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "username": "johndoe",
    "available": false,
    "suggestions": ["johndoe99", "johndoe_official", "the_johndoe"]
  }
}

10. Set Username (Step 2)

Purpose: Set the user's chosen username

Endpoint: POST {base_url}/auth/onboarding/username

Access Level: Public (requires valid onboarding token)

Request Body:

{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "username": "johndoe99"
}
Parameter Type Required Description Validation
onboardingToken string Yes Token from previous step Valid JWT
username string Yes Chosen username 3-30 chars, starts with letter, alphanumeric + underscore

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Username set",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "username": "johndoe99",
    "available": true,
    "currentStep": "USERNAME_SET",
    "nextStep": "CONTACT_VERIFIED",
    "nextStepMetadata": {
      "contactVerification": {
        "requiredContactType": "EMAIL",
        "requiresInput": true,
        "alreadyVerified": {
          "type": "PHONE",
          "maskedValue": "... ... ..50"
        },
        "reasons": ["TICKET_DELIVERY", "ACCOUNT_RECOVERY", "ACCOUNT_SECURITY"]
      }
    },
    "message": "Username set successfully"
  }
}

requiredContactType depends on signup method:


11. Initiate Contact Verification (Step 3)

Purpose: Send OTP to the user's secondary contact (email or phone)

Endpoint: POST {base_url}/auth/onboarding/contact/initiate

Access Level: Public (requires valid onboarding token)

Request Body:

{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "contactValue": "johndoe@gmail.com"
}
Parameter Type Required Description Validation
onboardingToken string Yes Token from previous step Valid JWT
contactValue string Conditional Email or phone to verify Required only if requiresInput = true

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP sent",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "maskedValue": "j......@g.....com",
    "contactType": "EMAIL",
    "expiresIn": 600,
    "verified": false
  }
}

12. Verify Contact OTP (Step 3 - continued)

Purpose: Submit the OTP sent to the secondary contact

Endpoint: POST {base_url}/auth/onboarding/contact/verify

Access Level: Public (requires valid onboarding token)

Request Body:

{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "otp": "847291"
}
Parameter Type Required Description
onboardingToken string Yes Current onboarding token
tempToken string Yes Token returned from initiate endpoint
otp string Yes 6-digit OTP code

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Contact verified",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "currentStep": "CONTACT_VERIFIED",
    "nextStep": "INTERESTS_SELECTED",
    "verified": true
  }
}

13. Edit Contact (Step 3 - inline edit)

Purpose: Change the contact value before verifying — no page navigation, inline only

Endpoint: POST {base_url}/auth/onboarding/contact/edit

Access Level: Public (requires valid onboarding token)

Request Body:

{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "newContactValue": "newemail@gmail.com"
}
Parameter Type Required Description
onboardingToken string Yes Current onboarding token
tempToken string Yes Token from initiate endpoint
newContactValue string Yes New email or phone number

Success Response: Same shape as initiate — returns a new tempToken and new maskedValue for the updated contact.

Previous OTP is invalidated immediately. A fresh OTP is sent to the new contact.

Duplicate contact handling:


14. Get Interest Categories

Purpose: Get all available interest categories for selection

Endpoint: GET {base_url}/interests/categories/all

Access Level: Public

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Categories retrieved",
  "action_time": "2025-01-24T10:30:45",
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "name": "Music",
      "icon": "🎵",
      "description": "Concerts, artists, playlists",
      "displayOrder": 1,
      "isActive": true
    },
    {
      "id": "550e8400-e29b-41d4-a716-446655440002",
      "name": "Sports",
      "icon": "⚽",
      "description": "Games, teams, fitness",
      "displayOrder": 2,
      "isActive": true
    }
  ]
}

15. Set Interests (Step 4)

Purpose: Save user's selected interests

Endpoint: POST {base_url}/auth/onboarding/interests

Access Level: Public (requires valid onboarding token)

Request Body:

{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "interestIds": [
    "550e8400-e29b-41d4-a716-446655440001",
    "550e8400-e29b-41d4-a716-446655440002",
    "550e8400-e29b-41d4-a716-446655440003"
  ]
}
Parameter Type Required Description Validation
onboardingToken string Yes Token from previous step Valid JWT
interestIds array Yes List of category UUIDs Min 3 items

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Interests saved",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "selectedInterests": ["Music", "Sports", "Technology"],
    "count": 3,
    "currentStep": "INTERESTS_SELECTED",
    "nextStep": "PROFILE_COMPLETED",
    "nextStepMetadata": {
      "firstName": "John",
      "lastName": "Doe",
      "username": "johndoe99",
      "suggestedDisplayName": "John Doe",
      "profilePicture": "https://lh3.googleusercontent.com/a/..."
    },
    "message": "Interests saved successfully"
  }
}

16. Set Profile (Step 5 - Final)

Purpose: Complete profile setup and finish onboarding

Endpoint: POST {base_url}/auth/onboarding/profile

Access Level: Public (requires valid onboarding token)

Request Headers:

Header Type Required Description
X-Device-Id string No Device fingerprint
X-Device-Name string No Device name
X-Platform string No IOS, ANDROID, or WEB

Request Body:

{
  "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "displayName": "John Doe",
  "bio": "Tech enthusiast | Music lover | Based in Dar es Salaam 🇹🇿",
  "profilePictureUrl": "https://storage.nextgate.com/profiles/abc123.jpg"
}
Parameter Type Required Description Validation
onboardingToken string Yes Token from previous step Valid JWT
displayName string Yes Public display name 1-50 characters
bio string No User biography Max 160 characters
profilePictureUrl string No Profile picture URL Valid URL

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Welcome to NextGate!",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "systemUsername": "su_550e8400-e29b-41d4-a716-446655440000",
    "username": "johndoe99",
    "displayName": "John Doe",
    "currentStep": "COMPLETED",
    "onboardingComplete": true,
    "message": "Welcome to NextGate!"
  }
}

17. Upload Profile Picture (Onboarding Step)

Purpose: Upload a profile picture during onboarding, before completing profile setup Endpoint: POST {base_url}/auth/onboarding/upload-profile-picture Access Level: Public (requires valid onboarding token) Request: multipart/form-data

Parameter Type Required Description Validation
file file Yes Profile picture file Image files only, max 25MB
X-Onboarding-Token header Yes Token from previous step Valid onboarding JWT

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Profile picture uploaded",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "fileName": "a3f1c2d4-...-uuid.jpg",
    "originalFileName": "my_photo.jpg",
    "objectKey": "profile/a3f1c2d4-...-uuid.jpg",
    "directory": "PROFILE",
    "contentType": "image/jpeg",
    "fileSize": 204800,
    "fileSizeFormatted": "200.0 KB",
    "permanentUrl": "https://files.nextgate.co.tz/bucket-id/profile/a3f1c2d4-...-uuid.jpg",
    "thumbnailUrl": "https://files.nextgate.co.tz/bucket-id/profile/a3f1c2d4-...-uuid.jpg",
    "blurHash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
    "fileExtension": ".jpg",
    "fileType": "IMAGE",
    "isImage": true,
    "isVideo": false,
    "isDocument": false,
    "isAudio": false,
    "width": 800,
    "height": 800,
    "dimensions": "800x800",
    "checksum": "d41d8cd98f00b204e9800998ecf8427e",
    "uploadedAt": "2025-01-24T10:30:45",
    "isPublic": true
  }
}

Use the returned permanentUrl as the value of profilePictureUrl in the subsequent Set Profile request.

This endpoint is only accessible when the user has completed the INTERESTS_SELECTED step. Calling it earlier will result in a 403 verification error.


Token Management

14. Refresh Access Token

Purpose: Get a new access token using refresh token (with rotation)

Endpoint: POST {base_url}/auth/token/refresh

Access Level: 🌐 Public

Authentication: None (refresh token in body)

Request JSON Sample:

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Token refreshed",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "message": "Tokens refreshed successfully"
  }
}

⚠️ Important: The refresh token is rotated on each use. Always store the new refreshToken from the response.

Error Response (Token Reuse Detected):

{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Security alert: Token reuse detected. Please login again.",
  "action_time": "2025-01-24T10:30:45",
  "data": "Security alert: Token reuse detected. Please login again."
}

15. Revoke Token

Purpose: Revoke a refresh token (logout)

Endpoint: POST {base_url}/auth/token/revoke

Access Level: 🌐 Public

Authentication: None

Request JSON Sample:

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Token revoked successfully",
  "action_time": "2025-01-24T10:30:45",
  "data": null
}

16. Refresh Onboarding Token

Purpose: Refresh onboarding token if it's about to expire

Endpoint: POST {base_url}/auth/token/refresh-onboarding

Access Level: 🌐 Public

Authentication: None

Request JSON Sample:

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Onboarding token refreshed",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "onboardingToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "currentStep": "USERNAME_SET",
    "nextStep": "INTERESTS_SELECTED",
    "expiresIn": 1800,
    "message": "Token refreshed successfully"
  }
}

Password Management

17. Initiate Forgot Password

Purpose: Start password reset flow

Endpoint: POST {base_url}/auth/password/forgot/initiate

Access Level: 🌐 Public

Authentication: None

Request JSON Sample:

{
  "identifier": "johndoe99"
}

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Request processed",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "maskedDestination": "j••••••@g••••.com",
    "sentTo": "EMAIL",
    "expiresIn": 600,
    "otpVerified": false,
    "passwordReset": false,
    "requiresChannelSelection": false,
    "message": "OTP sent to email"
  }
}

18. Verify Forgot Password OTP

Purpose: Verify OTP for password reset

Endpoint: POST {base_url}/auth/password/forgot/verify-otp

Access Level: 🌐 Public

Authentication: None

Request JSON Sample:

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

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP verified",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "resetToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "otpVerified": true,
    "passwordReset": false,
    "message": "OTP verified. Set your new password."
  }
}

19. Reset Forgotten Password

Purpose: Set new password after OTP verification

Endpoint: POST {base_url}/auth/password/forgot/reset

Access Level: 🌐 Public

Authentication: None

Request JSON Sample:

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

Request Body Parameters:

Parameter Type Required Description Validation
resetToken string Yes Token from verify-otp Valid JWT
newPassword string Yes New password Min 8 characters
confirmPassword string Yes Confirmation Must match newPassword

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Password reset successfully",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "otpVerified": true,
    "passwordReset": true,
    "message": "Password reset successfully"
  }
}

⚠️ Note: After password reset, all active sessions are revoked. User must login again.


20. Change Password (Authenticated)

Purpose: Change password for logged-in user

Endpoint: POST {base_url}/password/change

Access Level: 🔒 Protected

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer {accessToken}

Request JSON Sample:

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

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Password changed",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "success": true,
    "hadPassword": true,
    "message": "Password changed successfully"
  }
}

21. Set Password (for Passwordless Users)

Purpose: Set password for users who registered via OAuth or OTP only

Endpoint: POST {base_url}/password/set

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

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

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Password set successfully",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "success": true,
    "hadPassword": false,
    "message": "Password set successfully. You can now login with password."
  }
}

Session Management

22. Get All Sessions

Purpose: List all active sessions for the user

Endpoint: GET {base_url}/auth/sessions

Access Level: 🔒 Protected

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer {accessToken}
X-Session-Id string No Current session ID

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Sessions retrieved",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "sessions": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440001",
        "deviceId": "a1b2c3d4e5f6789",
        "deviceName": "Chrome on macOS",
        "platform": "WEB",
        "ipAddress": "192.168.1.1",
        "location": "Dar es Salaam, Tanzania",
        "lastActiveAt": "2025-01-24T10:30:45",
        "createdAt": "2025-01-20T08:00:00",
        "currentSession": true
      },
      {
        "id": "550e8400-e29b-41d4-a716-446655440002",
        "deviceId": "xyz789abc123",
        "deviceName": "Safari on iPhone",
        "platform": "IOS",
        "ipAddress": "192.168.1.2",
        "location": "Arusha, Tanzania",
        "lastActiveAt": "2025-01-23T15:20:00",
        "createdAt": "2025-01-15T12:00:00",
        "currentSession": false
      }
    ],
    "totalCount": 2
  }
}

23. Sign Out (Current Session)

Purpose: End the current session

Endpoint: POST {base_url}/auth/sessions/sign-out

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Signed out successfully",
  "action_time": "2025-01-24T10:30:45",
  "data": null
}

24. Sign Out All Sessions

Purpose: End all sessions including current (security logout)

Endpoint: POST {base_url}/auth/sessions/sign-out-all

Access Level: 🔒 Protected

Authentication: Bearer Token

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "All sessions terminated",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "terminatedCount": 4,
    "message": "All 4 sessions have been terminated. Please login again."
  }
}

25. Revoke Specific Session

Purpose: End a specific session by ID

Endpoint: DELETE {base_url}/auth/sessions/{sessionId}

Access Level: 🔒 Protected

Authentication: Bearer Token

Path Parameters:

Parameter Type Required Description
sessionId string Yes Session UUID to revoke

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Session revoked",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "sessionId": "550e8400-e29b-41d4-a716-446655440002",
    "message": "Session has been terminated"
  }
}

Device Management

26. Get All Devices

Purpose: List all registered/trusted devices

Endpoint: GET {base_url}/auth/devices

Access Level: 🔒 Protected

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer {accessToken}
X-Device-Id string No Current device ID

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Devices retrieved",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "devices": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440001",
        "deviceId": "a1b2c3d4e5f6789",
        "deviceName": "Chrome on macOS",
        "platform": "WEB",
        "lastIpAddress": "192.168.1.1",
        "lastLocation": "Dar es Salaam, Tanzania",
        "lastActiveAt": "2025-01-24T10:30:45",
        "firstSeenAt": "2025-01-01T08:00:00",
        "trustLevel": "TRUSTED",
        "isCurrentDevice": true
      }
    ],
    "totalCount": 1
  }
}

27. Remove Device

Purpose: Remove a device from trusted devices (revokes all sessions on that device)

Endpoint: DELETE {base_url}/auth/devices/{deviceId}

Access Level: 🔒 Protected

Authentication: Bearer Token

Path Parameters:

Parameter Type Required Description
deviceId string Yes Device record UUID

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device removed",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "deviceId": "550e8400-e29b-41d4-a716-446655440002",
    "sessionsRevoked": 2,
    "message": "Device removed and 2 sessions terminated"
  }
}

Account Linking

28. Get Linked Accounts

Purpose: List all linked OAuth providers and identifiers

Endpoint: GET {base_url}/auth/linked-accounts

Access Level: 🔒 Protected

Authentication: Bearer Token

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Linked accounts retrieved",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "linkedAccounts": [
      {
        "type": "EMAIL",
        "value": "john.doe@gmail.com",
        "isPrimary": true,
        "isVerified": true,
        "linkedAt": "2025-01-01T08:00:00"
      },
      {
        "type": "PHONE",
        "value": "+255712345678",
        "isPrimary": false,
        "isVerified": true,
        "linkedAt": "2025-01-05T10:00:00"
      },
      {
        "type": "OAUTH",
        "provider": "GOOGLE",
        "email": "john.doe@gmail.com",
        "linkedAt": "2025-01-01T08:00:00"
      }
    ],
    "hasPassword": true,
    "canRemoveEmail": true,
    "canRemovePhone": true
  }
}

Purpose: Add a new email to the account

Endpoint: POST {base_url}/auth/linked-accounts/email/link

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

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

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Verification email sent",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "email": "j••••••@c••••••.com",
    "expiresIn": 600,
    "message": "Verification code sent"
  }
}

Purpose: Add a new phone number to the account

Endpoint: POST {base_url}/auth/linked-accounts/phone/link

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

{
  "phone": "+255787654321"
}

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Verification SMS sent",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "phone": "••• ••• ••21",
    "expiresIn": 600,
    "message": "Verification code sent"
  }
}

Endpoint: POST {base_url}/auth/linked-accounts/oauth/link

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

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

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OAuth provider linked",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "provider": "APPLE",
    "email": "john.doe@icloud.com",
    "linkedAt": "2025-01-24T10:30:45",
    "message": "Apple account linked successfully"
  }
}

Purpose: Remove an OAuth provider from the account

Endpoint: DELETE {base_url}/auth/linked-accounts/oauth/{provider}

Access Level: 🔒 Protected

Authentication: Bearer Token

Path Parameters:

Parameter Type Required Description
provider string Yes GOOGLE or APPLE

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OAuth provider unlinked",
  "action_time": "2025-01-24T10:30:45",
  "data": {
    "provider": "APPLE",
    "message": "Apple account unlinked"
  }
}

Error Response (Last Login Method):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot unlink: This is your only login method.",
  "action_time": "2025-01-24T10:30:45",
  "data": "Cannot unlink: This is your only login method."
}

Error Reference

Standard Error Codes

HTTP Code Status Common Causes
400 BAD_REQUEST Invalid request data, item already exists
401 UNAUTHORIZED Missing/invalid/expired token
403 FORBIDDEN Invalid OTP, max attempts exceeded, blocked
404 NOT_FOUND Resource doesn't exist
422 UNPROCESSABLE_ENTITY Validation errors
429 TOO_MANY_REQUESTS Rate limit exceeded
500 INTERNAL_SERVER_ERROR Server error

Authentication-Specific Errors

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

Frontend Integration Checklist

Before Starting

Authentication Flow

Onboarding Flow

Token Management

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:

Response Format Standard: All responses follow GlobeSuccessResponseBuilder or GlobeFailureResponseBuilder format:

{
  "success": true/false,
  "httpStatus": "OK/BAD_REQUEST/etc",
  "message": "Human readable message",
  "action_time": "2025-01-11T15:20:00",
  "data": { ... }
}

DEVICE SECURITY ARCHITECTURE

The Problem We're Solving

Without hardware-bound keys:
─────────────────────────────────────────────────────────
Attacker steals victim's password
    ↓
Attacker generates fake deviceId: "fake-device-123"
    ↓
Attacker logs in → Server asks for OTP
    ↓
Attacker does SIM swap / social engineering → gets OTP
    ↓
Attacker is now "trusted" forever 😱
    ↓
Victim can't kick attacker out (attacker has valid device)


With hardware-bound keys:
─────────────────────────────────────────────────────────
Attacker steals victim's password
    ↓
Attacker tries to login with fake deviceId
    ↓
Server: "Sign this challenge with your private key"
    ↓
Attacker: "I don't have the private key..." 😤
    ↓
Private key is locked inside victim's phone hardware
    ↓
Attack FAILS ✅

Asymmetric Cryptography (Key Pair Concept)

┌─────────────────────────────────────────────────────────────┐
│                    ASYMMETRIC CRYPTOGRAPHY                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Private Key                      Public Key               │
│   ───────────                      ──────────               │
│   • Secret                         • Shareable              │
│   • Never leaves device            • Stored on server       │
│   • Used to SIGN                   • Used to VERIFY         │
│   • Locked in hardware             • Anyone can have it     │
│                                                             │
│   ┌─────────────┐                 ┌─────────────┐          │
│   │ Private Key │──── generates ──▶│ Public Key  │          │
│   │    🔐       │                  │    🔓       │          │
│   └─────────────┘                  └─────────────┘          │
│         │                                │                  │
│         ▼                                ▼                  │
│   Sign("hello")                    Verify(signature)        │
│         │                                │                  │
│         ▼                                ▼                  │
│   "MEUCIQC7..."                    true / false             │
│   (signature)                                               │
│                                                             │
│   KEY POINT: You CANNOT derive private key from public key  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Platform Security Overview

┌─────────────────────────────────────────────────────────────┐
│                         iOS DEVICE                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────────────────────────────────────────────────┐  │
│   │                    Main Processor                    │  │
│   │   Your app lives here                                │  │
│   │   Can request signatures                             │  │
│   │   CANNOT access private key                          │  │
│   └─────────────────────────────────────────────────────┘  │
│                           │                                 │
│                           │ "Please sign this"              │
│                           ▼                                 │
│   ┌─────────────────────────────────────────────────────┐  │
│   │              SECURE ENCLAVE (Separate Chip)          │  │
│   │   ┌─────────────────────────────────────────────┐   │  │
│   │   │   Private Key 🔐                            │   │  │
│   │   │   • Generated HERE                          │   │  │
│   │   │   • Stored HERE                             │   │  │
│   │   │   • NEVER leaves                            │   │  │
│   │   │   • Cannot be read by main processor        │   │  │
│   │   │   • Cannot be extracted even if jailbroken  │   │  │
│   │   └─────────────────────────────────────────────┘   │  │
│   │   Returns: signature (NOT the key)                   │  │
│   └─────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                       ANDROID DEVICE                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Option A: StrongBox (Dedicated Security Chip)             │
│   • Separate hardware chip (like iOS Secure Enclave)        │
│   • Best security                                           │
│   • Available on Pixel 3+, Samsung S10+, etc.               │
│                                                             │
│   Option B: TEE (Trusted Execution Environment)             │
│   • Isolated area within main processor                     │
│   • Very good security                                      │
│   • Available on most Android 7+ devices                    │
│                                                             │
│   Both use Android Keystore API                             │
│   Same result: Private key cannot be extracted              │
│                                                             │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                       WEB BROWSER                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Problem: No dedicated hardware security                   │
│   Solution: PKCE-style ephemeral keys (explained below)     │
│                                                             │
│   • Generate keypair in MEMORY (not stored)                 │
│   • Session-bound (dies when tab closes)                    │
│   • Combined with fingerprint + token binding               │
│   • NEVER fully trusted (always require OTP for sensitive)  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Platform Security Comparison

Aspect iOS Android Web
Key Storage Secure Enclave StrongBox / TEE Memory only
Hardware Protected ✅ Yes ✅ Yes ❌ No
Key Extractable ❌ Never ❌ Never N/A (ephemeral)
Forgery Possible ❌ No ❌ No ⚠️ Harder
Trust Level ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
Trust Duration 30 days 30 days 7 days
Sensitive Actions Some without OTP Some without OTP ALWAYS OTP

Why Attacker Cannot Forge (Mobile)

What attacker has access to:
──────────────────────────────────────────────────────────────
✅ Victim's email/username (public or leaked)
✅ Victim's password (phishing, data breach, etc.)
✅ Victim's deviceId (could intercept network traffic)
✅ Victim's publicKey (stored on server, not secret)
✅ Can request fresh nonce anytime

What attacker CANNOT get:
──────────────────────────────────────────────────────────────
❌ Victim's privateKey (locked in hardware)
   • Cannot extract from Secure Enclave
   • Cannot extract even with physical device access
   • Cannot extract even if device is jailbroken

Without privateKey:
──────────────────────────────────────────────────────────────
❌ Cannot create valid signature
❌ Server rejects login
❌ Attack fails

Challenge/Nonce Purpose

Without nonce:
──────────────────────────────────────────────────────────────
1. Victim logs in legitimately
2. Attacker intercepts: { deviceId, signature }
3. Attacker replays exact same request
4. Server: "Signature valid!" ✅
5. Attacker is in 😱

Problem: Signature is always the same for same deviceId


With nonce:
──────────────────────────────────────────────────────────────
1. Victim logs in legitimately
   - Gets nonce "ch_abc123"
   - Signs "ch_abc123|timestamp|deviceId"
   
2. Attacker intercepts the request
   
3. Attacker replays exact same request 1 minute later
   
4. Server: "Nonce ch_abc123 already used/expired!" ❌

5. Attacker tries to get new nonce and replay
   - Gets nonce "ch_xyz789"
   - But old signature was for "ch_abc123"
   - Signature doesn't match new nonce ❌
   
6. Attacker cannot create new signature
   - Needs privateKey to sign "ch_xyz789|..."
   - privateKey is in victim's phone ❌

7. Attack fails ✅


Key insight: 
──────────────────────────────────────────────────────────────
Nonce makes each signature UNIQUE and TIME-LIMITED
Even captured valid signatures become useless after ~60 seconds

WEB SECURITY MODEL (PKCE-Style)

The Problem: Web Cannot Keep Secrets

Mobile App:
────────────────────────────────────────────────────────
✅ Compiled binary (hard to reverse engineer)
✅ Secure storage (Keychain, Keystore)
✅ Hardware protection (Secure Enclave, TEE)
✅ Can store secrets safely


Web Browser:
────────────────────────────────────────────────────────
❌ JavaScript is readable (View Source)
❌ localStorage/IndexedDB accessible via DevTools
❌ No hardware-protected storage
❌ Any "secret" can be extracted

If we store a private key in browser:
→ Open DevTools → Application → IndexedDB → Copy the key → Use anywhere 😱

The Solution: PKCE-Style Ephemeral Keys

OAuth2 had the same problem. Their solution: Don't store a secret. Generate a temporary one-time proof.

┌─────────────────────────────────────────────────────────────┐
│              WEB DEVICE AUTH (PKCE-Style)                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Instead of:                                                │
│  Store private key → Sign challenges                        │
│  (Private key can be stolen from IndexedDB)                 │
│                                                             │
│  We do:                                                     │
│  Each session: Generate fresh keypair in MEMORY             │
│  Register public key with server for THIS SESSION           │
│  Private key lives only in JavaScript memory                │
│  When tab closes → key is gone → nothing to steal           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Web Security Layers

┌─────────────────────────────────────────────────────────────┐
│                    WEB SECURITY MODEL                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  LAYER 1: Session-Bound Cryptographic Proof                 │
│  • Generate keypair in memory (not stored)                  │
│  • Proves: "Same browser tab that started auth"             │
│  • Prevents: Request interception/replay                    │
│                                                             │
│  LAYER 2: Browser Fingerprint                               │
│  • Collect browser characteristics                          │
│  • Proves: "Likely same browser/device"                     │
│  • Prevents: Token theft to different browser               │
│                                                             │
│  LAYER 3: Bound Tokens                                      │
│  • Tokens bound to fingerprint + IP range                   │
│  • Server validates on each request                         │
│  • Prevents: Token theft/export                             │
│                                                             │
│  LAYER 4: Never Fully Trust                                 │
│  • Web devices NEVER get "trusted" status                   │
│  • Sensitive actions ALWAYS require OTP                     │
│  • Shorter token lifetime than mobile                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Web vs Mobile Trust Levels

┌─────────────────────────────────────────────────────────────┐
│                  TRUST LEVEL COMPARISON                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  MOBILE (iOS/Android):                                      │
│  After OTP verification:                                    │
│  • Device becomes "TRUSTED"                                 │
│  • Trust lasts 30 days                                      │
│  • Password-only login allowed                              │
│  • Most actions without re-auth                             │
│  • Hardware key proves device identity                      │
│                                                             │
│  WEB (Browser):                                             │
│  After OTP verification:                                    │
│  • Device is "RECOGNIZED" (not trusted)                     │
│  • Recognition lasts 7 days (shorter)                       │
│  • Password-only login allowed for basic actions            │
│  • Sensitive actions ALWAYS need OTP:                       │
│    • Change password                                        │
│    • Change email/phone                                     │
│    • View payment methods                                   │
│    • Delete account                                         │
│    • Large purchases                                        │
│  • Session-bound keys (weaker than hardware)                │
│                                                             │
│  WHY THE DIFFERENCE:                                        │
│  Mobile: Hardware guarantees "this is THE device"           │
│  Web: Software only guarantees "probably same browser"      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

AUTHENTICATION FLOWS

Device Registration (First App Launch - Mobile)

┌──────────────┐                              ┌──────────────┐
│    Device    │                              │    Server    │
└──────┬───────┘                              └──────┬───────┘
       │                                             │
       │  1. Generate key pair in hardware           │
       │     Private key → Secure Enclave            │
       │     Public key → exportable                 │
       │                                             │
       │  2. GET /auth/challenge                     │
       │────────────────────────────────────────────▶│
       │                                             │
       │  3. { nonce: "ch_abc123", expiresIn: 60 }   │
       │◀────────────────────────────────────────────│
       │                                             │
       │  4. Sign nonce with private key             │
       │     message = "ch_abc123|timestamp|deviceId"│
       │     signature = sign(message, privateKey)   │
       │                                             │
       │  5. POST /auth/device/register              │
       │     {                                       │
       │       deviceId: "ios_xyz...",               │
       │       publicKey: "MFkw...",                 │
       │       nonce: "ch_abc123",                   │
       │       timestamp: 1736611200000,             │
       │       signature: "MEUC...",                 │
       │       platform: "IOS"                       │
       │     }                                       │
       │────────────────────────────────────────────▶│
       │                                             │
       │                      6. Verify signature    │
       │                         using publicKey     │
       │                                             │
       │                      7. Store:              │
       │                         deviceId → publicKey│
       │                                             │
       │  8. { success: true, deviceId: "ios_xyz" }  │
       │◀────────────────────────────────────────────│
       │                                             │

Login Flow (Mobile - With Device Signature)

┌──────────────┐                              ┌──────────────┐
│    Device    │                              │    Server    │
└──────┬───────┘                              └──────┬───────┘
       │                                             │
       │  1. GET /auth/challenge                     │
       │────────────────────────────────────────────▶│
       │                                             │
       │  2. { nonce: "ch_xyz789", expiresIn: 60 }   │
       │◀────────────────────────────────────────────│
       │                                             │
       │  3. Sign: signature = sign(                 │
       │       "ch_xyz789|1736611200000|ios_xyz...", │
       │       privateKey                            │
       │     )                                       │
       │                                             │
       │  4. POST /auth/login                        │
       │     {                                       │
       │       identifier: "alex@email.com",         │
       │       password: "***",                      │
       │       deviceAuth: {                         │
       │         deviceId: "ios_xyz...",             │
       │         nonce: "ch_xyz789",                 │
       │         timestamp: 1736611200000,           │
       │         signature: "MEUC...",               │
       │         platform: "IOS"                     │
       │       }                                     │
       │     }                                       │
       │────────────────────────────────────────────▶│
       │                                             │
       │                      5. Validate nonce      │
       │                         (exists in Redis?)  │
       │                                             │
       │                      6. Delete nonce        │
       │                         (one-time use)      │
       │                                             │
       │                      7. Get publicKey       │
       │                         for deviceId        │
       │                                             │
       │                      8. Verify signature    │
       │                                             │
       │                      9. Check password      │
       │                                             │
       │                     10. Check device trust  │
       │                         status              │
       │                                             │
       │  11. { accessToken, refreshToken, ... }     │
       │◀────────────────────────────────────────────│
       │                                             │

Web Session Flow (PKCE-Style)

┌──────────────┐                              ┌──────────────┐
│   Browser    │                              │    Server    │
└──────┬───────┘                              └──────┬───────┘
       │                                             │
       │  User opens login page                      │
       │                                             │
       │  1. Generate keypair IN MEMORY              │
       │     const keyPair = await crypto.subtle    │
       │       .generateKey(ECDSA, P-256)            │
       │     ⚠️ NOT stored anywhere                  │
       │     ⚠️ Lives only in JS variable            │
       │                                             │
       │  2. Generate browser fingerprint            │
       │     • Screen size, timezone, language       │
       │     • Canvas hash, WebGL renderer           │
       │     • → Hash all into single ID             │
       │                                             │
       │  3. POST /auth/web/session                  │
       │     {                                       │
       │       publicKey: "MFkw...",                 │
       │       fingerprint: "fp_abc123..."           │
       │     }                                       │
       │────────────────────────────────────────────▶│
       │                                             │
       │                      4. Generate sessionId  │
       │                      5. Store in Redis:     │
       │                         sessionId →         │
       │                           publicKey,        │
       │                           fingerprint,      │
       │                           ipAddress         │
       │                         TTL: 10 minutes     │
       │                                             │
       │  6. { sessionId: "ws_xyz...",               │
       │       nonce: "ch_abc..." }                  │
       │◀────────────────────────────────────────────│
       │                                             │
       │  User enters email + password               │
       │                                             │
       │  7. Sign the nonce                          │
       │     signature = sign(                       │
       │       nonce + timestamp + sessionId,        │
       │       privateKey  ← still in memory         │
       │     )                                       │
       │                                             │
       │  8. POST /auth/login                        │
       │     {                                       │
       │       identifier: "alex@email.com",         │
       │       password: "***",                      │
       │       webAuth: {                            │
       │         sessionId: "ws_xyz...",             │
       │         nonce: "ch_abc...",                 │
       │         timestamp: 1736611200000,           │
       │         signature: "MEUC...",               │
       │         fingerprint: "fp_abc123..."         │
       │       }                                     │
       │     }                                       │
       │────────────────────────────────────────────▶│
       │                                             │
       │                      9. Get session from    │
       │                         Redis               │
       │                                             │
       │                     10. Verify:             │
       │                         • Session exists    │
       │                         • Not expired       │
       │                         • Signature valid   │
       │                         • Fingerprint match │
       │                         • IP in range       │
       │                                             │
       │                     11. Delete session      │
       │                         (one-time use)      │
       │                                             │
       │                     12. Create BOUND tokens │
       │                         (bound to fp + IP)  │
       │                                             │
       │  13. {                                      │
       │        accessToken: "...",                  │
       │        refreshToken: "...",                 │
       │        device: { trusted: false }           │
       │      }                                      │
       │◀────────────────────────────────────────────│
       │                                             │

SCALABLE ARCHITECTURE

┌─────────────────────────────────────────────────────────────────────────────────┐
│                          NEXTGATE SECURITY ARCHITECTURE                          │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│  ┌───────────────────────────────────────────────────────────────────────────┐  │
│  │                         API GATEWAY / LOAD BALANCER                        │  │
│  │                    (Rate Limiting, DDoS Protection)                        │  │
│  └───────────────────────────────────────────────────────────────────────────┘  │
│                                  │                                               │
│                                  ▼                                               │
│  ┌───────────────────────────────────────────────────────────────────────────┐  │
│  │                        AUTH SERVICE (Stateless)                            │  │
│  │  ┌─────────────────────────────────────────────────────────────────────┐  │  │
│  │  │                    Challenge/Nonce Service                           │  │  │
│  │  │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐                 │  │  │
│  │  │  │ Node 1  │  │ Node 2  │  │ Node 3  │  │ Node N  │  (Stateless)    │  │  │
│  │  │  └─────────┘  └─────────┘  └─────────┘  └─────────┘                 │  │  │
│  │  │       │            │            │            │                       │  │  │
│  │  │       └────────────┴─────┬──────┴────────────┘                       │  │  │
│  │  │                          │                                           │  │  │
│  │  │                          ▼                                           │  │  │
│  │  │              ┌───────────────────────┐                               │  │  │
│  │  │              │   Redis Cluster       │  (Nonce storage, 60s TTL)     │  │  │
│  │  │              │   (High Availability) │                               │  │  │
│  │  │              └───────────────────────┘                               │  │  │
│  │  └─────────────────────────────────────────────────────────────────────┘  │  │
│  │                                                                            │  │
│  │  ┌─────────────────────────────────────────────────────────────────────┐  │  │
│  │  │                  Signature Verification Service                      │  │  │
│  │  │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐                 │  │  │
│  │  │  │ Node 1  │  │ Node 2  │  │ Node 3  │  │ Node N  │  (Stateless)    │  │  │
│  │  │  └─────────┘  └─────────┘  └─────────┘  └─────────┘                 │  │  │
│  │  │       │            │            │            │                       │  │  │
│  │  │       └────────────┴─────┬──────┴────────────┘                       │  │  │
│  │  │                          │                                           │  │  │
│  │  │                          ▼                                           │  │  │
│  │  │     ┌───────────────────────────────────────────────────────┐       │  │  │
│  │  │     │              PostgreSQL (Primary-Replica)              │       │  │  │
│  │  │     │  device_keys table (deviceId → publicKey)              │       │  │  │
│  │  │     │  Cached in Redis for fast lookups                      │       │  │  │
│  │  │     └───────────────────────────────────────────────────────┘       │  │  │
│  │  └─────────────────────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│  SCALABILITY:                                                                    │
│  • Challenge generation: < 5ms (any node can generate)                           │
│  • Signature verification: < 10ms (pure computation)                             │
│  • Public keys cached in Redis (1-hour TTL)                                      │
│  • Can handle 10,000+ logins/second per node                                     │
│  • Horizontal scaling: just add more nodes                                       │
│                                                                                  │
└──────────────────────────────────────────────────────────────────────────────────┘

TOKEN BINDING (Web Extra Protection)

┌─────────────────────────────────────────────────────────────┐
│                     TOKEN BINDING                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Traditional Token:                                         │
│  {                                                          │
│    "sub": "user_123",                                       │
│    "exp": 1736614800                                        │
│  }                                                          │
│  Problem: Anyone with this token can use it                 │
│                                                             │
│                                                             │
│  Bound Token (What we do for web):                          │
│  {                                                          │
│    "sub": "user_123",                                       │
│    "exp": 1736614800,                                       │
│    "device_fp": "fp_abc123...",     ← Must match           │
│    "ip_hash": "a1b2c3...",          ← Must be in range     │
│    "platform": "WEB"                 ← Affects trust level  │
│  }                                                          │
│                                                             │
│  On every request, server checks:                           │
│  • Current fingerprint ≈ token's device_fp                  │
│  • Current IP in same /24 range as token's IP               │
│  • If mismatch → reject OR require re-auth                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

BROWSER FINGERPRINT

┌─────────────────────────────────────────────────────────────┐
│                  BROWSER FINGERPRINT                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Components collected:                                      │
│  1. Screen: "1920x1080"                                     │
│  2. Color Depth: 24                                         │
│  3. Timezone: "Africa/Dar_es_Salaam"                        │
│  4. Language: "en-US"                                       │
│  5. Platform: "MacIntel"                                    │
│  6. CPU Cores: 8                                            │
│  7. Memory: 8 (GB, approximate)                             │
│  8. Canvas Hash: "a1b2c3..." (drawn image fingerprint)      │
│  9. WebGL Renderer: "Apple M1"                              │
│  10. Audio Context fingerprint                              │
│                                                             │
│  All combined → SHA256 → "fp_7f9a2b3c..."                   │
│                                                             │
│  Stability:                                                 │
│  • ~90% of users have unique fingerprint                    │
│  • Changes if: browser update, OS update, new monitor       │
│  • We allow ~15% variance (fuzzy matching)                  │
│                                                             │
│  Privacy Note:                                              │
│  • NOT used for tracking users                              │
│  • Only to verify "same browser" for security               │
│  • Stored hashed, associated with user session              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

ATTACK SCENARIO ANALYSIS

ATTACK 1: Steal the private key (Mobile)
────────────────────────────────────────────────────────────────
Attacker: Tries to extract key from device
Result: Key is in Secure Enclave / StrongBox
        Cannot be extracted even with physical access
Verdict: ❌ FAILED


ATTACK 2: Steal the private key (Web)
────────────────────────────────────────────────────────────────
Attacker: Opens DevTools, looks for private key
Result: Key is in JavaScript memory, not in storage
        When attacker opens DevTools, they're in THEIR session
        with THEIR keypair, not victim's
Verdict: ❌ FAILED


ATTACK 3: Intercept and replay request
────────────────────────────────────────────────────────────────
Attacker: Captures { sessionId, nonce, signature }
Attacker: Replays the exact request
Server: "Nonce already used/expired"
Verdict: ❌ FAILED (nonce is one-time use)


ATTACK 4: Get new nonce, use old signature
────────────────────────────────────────────────────────────────
Attacker: Gets new nonce "ch_NEW..."
Attacker: Uses old signature (signed for "ch_OLD...")
Server: Signature doesn't match nonce "ch_NEW..."
Verdict: ❌ FAILED (signature bound to specific nonce)


ATTACK 5: Steal tokens from victim's browser
────────────────────────────────────────────────────────────────
Attacker: Uses XSS to steal accessToken
Attacker: Uses token from different browser/IP
Server: Fingerprint mismatch! IP range mismatch!
Verdict: ❌ FAILED (tokens bound to browser + IP)


ATTACK 6: Create fake session from different browser
────────────────────────────────────────────────────────────────
Attacker: Knows victim's email + password
Attacker: Creates own session (own keypair, own fingerprint)
Attacker: Logs in successfully... BUT
Server: "New device detected, OTP required"
Attacker: Doesn't have victim's phone
Verdict: ❌ FAILED (OTP still required for new device)

SUMMARY: WHAT WE STORE WHERE

┌─────────────────────────────────────────────────────────────┐
│              WHAT WE STORE WHERE (MOBILE)                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ON DEVICE (Secure Storage):                                │
│  ✅ Private key (in Secure Enclave / Keystore)              │
│  ✅ deviceId (identifier)                                   │
│  ✅ accessToken (short-lived)                               │
│  ✅ refreshToken (long-lived)                               │
│                                                             │
│  ON SERVER:                                                 │
│  ✅ Public key (for signature verification)                 │
│  ✅ deviceId → userId mapping                               │
│  ✅ Trust status and expiry                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│              WHAT WE STORE WHERE (WEB)                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  IN BROWSER:                                                │
│  ✅ accessToken (short-lived, bound)                        │
│  ✅ refreshToken (medium-lived, bound)                      │
│  ✅ deviceId (just an identifier, not secret)               │
│  ❌ NO private keys stored                                  │
│  ❌ NO long-term secrets                                    │
│                                                             │
│  IN MEMORY ONLY (during session):                           │
│  ✅ Ephemeral keypair (dies when tab closes)                │
│                                                             │
│  ON SERVER:                                                 │
│  ✅ Session public key (Redis, 10-min TTL)                  │
│  ✅ Browser fingerprint hash                                │
│  ✅ IP range for token binding                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

DEVICE SECURITY API ENDPOINTS

1. Get Challenge (All Platforms)

GET /api/v1/auth/challenge

Called before any authenticated action (login, register device, refresh token).

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Challenge generated",
  "action_time": "2025-01-11T16:00:00Z",
  "data": {
    "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
    "expiresIn": 60,
    "expiresAt": "2025-01-11T16:01:00Z"
  }
}

2. Register Device (Mobile - First Launch)

POST /api/v1/auth/device/register

Called on first app launch to register the device's public key.

Request:

{
  "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...",
  "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
  "timestamp": 1736611200000,
  "signature": "MEUCIQC7...",
  "platform": "IOS"
}
Field Description
deviceId SHA256 hash of public key, prefixed with platform
publicKey Base64-encoded ECDSA P-256 public key
nonce Challenge from /auth/challenge
timestamp Current time in milliseconds
signature Sign(nonce|timestamp|deviceId, privateKey)
platform IOS, ANDROID, or WEB

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device registered successfully",
  "action_time": "2025-01-11T16:00:05Z",
  "data": {
    "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "registered": true,
    "securityLevel": "SECURE_ENCLAVE",
    "note": "Device will be linked to user account on login/signup"
  }
}

Response (Invalid Signature):

{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Invalid device signature",
  "action_time": "2025-01-11T16:00:05Z",
  "data": {
    "code": "INVALID_SIGNATURE",
    "suggestion": "Ensure keys are generated correctly"
  }
}

3. Initialize Web Session (Web Only)

POST /api/v1/auth/web/session

Called when user opens login page in browser.

Request:

{
  "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...",
  "fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c"
}
Field Description
publicKey Ephemeral public key (generated in memory)
fingerprint Browser fingerprint hash

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Web session initialized",
  "action_time": "2025-01-11T16:00:00Z",
  "data": {
    "sessionId": "ws_abc123xyz789",
    "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
    "expiresIn": 600,
    "expiresAt": "2025-01-11T16:10:00Z"
  }
}

4. Login with Device Auth (Mobile)

POST /api/v1/auth/login

Request:

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

Response (Success - Trusted Device):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2025-01-11T16:00:10Z",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "requiresOtp": false,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/..."
    },
    "device": {
      "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
      "trusted": true,
      "securityLevel": "SECURE_ENCLAVE",
      "trustExpiresAt": "2025-02-10T16:00:10Z"
    }
  }
}

Response (New Device - OTP Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "New device detected. Please verify with OTP.",
  "action_time": "2025-01-11T16:00:10Z",
  "data": {
    "requiresOtp": true,
    "otpReason": "NEW_DEVICE",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "otpSentTo": "+255*****678",
    "otpMethod": "SMS",
    "expiresAt": "2025-01-11T16:10:00Z",
    "device": {
      "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
      "isNew": true,
      "securityLevel": "SECURE_ENCLAVE"
    }
  }
}

Response (Invalid Signature):

{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Invalid device signature",
  "action_time": "2025-01-11T16:00:10Z",
  "data": {
    "code": "INVALID_SIGNATURE",
    "suggestion": "Please ensure your app is up to date"
  }
}

Response (Nonce Expired/Used):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Challenge expired or already used",
  "action_time": "2025-01-11T16:00:10Z",
  "data": {
    "code": "INVALID_NONCE",
    "suggestion": "Request a new challenge and try again"
  }
}

5. Login with Web Auth (Web Browser)

POST /api/v1/auth/login

Request:

{
  "identifier": "alex@example.com",
  "password": "MySecurePass123!",
  "webAuth": {
    "sessionId": "ws_abc123xyz789",
    "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
    "timestamp": 1736611200000,
    "signature": "MEUCIQC7...",
    "fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c"
  }
}

Response (Success - Web is NEVER fully trusted):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2025-01-11T16:00:10Z",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "requiresOtp": false,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson"
    },
    "device": {
      "trusted": false,
      "recognized": true,
      "platform": "WEB",
      "recognitionExpiresAt": "2025-01-18T16:00:10Z",
      "restrictions": [
        "OTP required for password change",
        "OTP required for email change",
        "OTP required for phone change",
        "OTP required for payment actions",
        "OTP required for account deletion"
      ]
    },
    "tokenBinding": {
      "boundToFingerprint": true,
      "boundToIpRange": true,
      "note": "Token will be invalidated if used from different browser/network"
    }
  }
}

6. Verify Device OTP (After New Device Detected)

POST /api/v1/auth/device/verify-otp

Request:

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

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device verified and trusted",
  "action_time": "2025-01-11T16:01:00Z",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson"
    },
    "device": {
      "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
      "trusted": true,
      "securityLevel": "SECURE_ENCLAVE",
      "trustExpiresAt": "2025-02-10T16:01:00Z"
    }
  }
}

7. Refresh Token (With Device Signature)

POST /api/v1/auth/token/refresh

Refresh tokens also require device signature to prevent stolen refresh token usage.

Request (Mobile):

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

Request (Web):

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

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Token refreshed",
  "action_time": "2025-01-11T17:00:00Z",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600
  }
}

Response (Device Mismatch):

{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Token does not belong to this device",
  "action_time": "2025-01-11T17:00:00Z",
  "data": {
    "code": "DEVICE_MISMATCH",
    "suggestion": "Please login again"
  }
}

DEVICE KEYS DATABASE SCHEMA

-- Device Keys Table (stores public keys for verification)
CREATE TABLE device_keys (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES accounts(id) ON DELETE CASCADE,
    device_id VARCHAR(64) NOT NULL UNIQUE,
    
    -- Cryptographic identity
    public_key TEXT NOT NULL,
    key_algorithm VARCHAR(20) DEFAULT 'EC_P256',
    
    -- Device metadata (server-derived from User-Agent)
    platform VARCHAR(20) NOT NULL,  -- IOS, ANDROID, WEB
    device_name VARCHAR(255),
    security_level VARCHAR(20),     -- SECURE_ENCLAVE, STRONGBOX, TEE, SOFTWARE
    
    -- Trust status
    is_trusted BOOLEAN DEFAULT FALSE,
    trust_expires_at TIMESTAMP,
    
    -- Activity tracking
    last_active_at TIMESTAMP DEFAULT NOW(),
    last_ip_address VARCHAR(45),
    last_location VARCHAR(255),
    
    -- Audit
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    
    CONSTRAINT unique_user_device UNIQUE (user_id, device_id)
);

CREATE INDEX idx_device_keys_user_id ON device_keys(user_id);
CREATE INDEX idx_device_keys_device_id ON device_keys(device_id);
CREATE INDEX idx_device_keys_last_active ON device_keys(last_active_at);


-- Web Sessions Table (Redis is preferred, but DB fallback)
CREATE TABLE web_sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id VARCHAR(64) NOT NULL UNIQUE,
    
    -- Session data
    public_key TEXT NOT NULL,
    fingerprint_hash VARCHAR(64) NOT NULL,
    ip_address VARCHAR(45),
    
    -- Expiry
    expires_at TIMESTAMP NOT NULL,
    used_at TIMESTAMP,  -- NULL = not used yet
    
    -- Audit
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_web_sessions_session_id ON web_sessions(session_id);
CREATE INDEX idx_web_sessions_expires ON web_sessions(expires_at);

REDIS KEYS STRUCTURE

# Nonce/Challenge storage (60 second TTL)
nonce:{nonce_value} = {ip_address}|{created_timestamp}

# Web session storage (10 minute TTL)
websession:{session_id} = {
  "publicKey": "MFkw...",
  "fingerprint": "fp_abc123",
  "ipAddress": "196.41.xxx.xxx",
  "createdAt": 1736611200000
}

# Public key cache (1 hour TTL)
pubkey:{device_id} = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."

# Device trust cache (matches trust expiry)
devicetrust:{device_id} = {
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "trusted": true,
  "expiresAt": 1739203200000
}

Problem: If username is used in JWT tokens, changing username requires logout (bad UX).

Solution: Separate system identifier from display username.

Field Type Purpose Can Change? Used In
id UUID Primary key ❌ Never DB relations
systemUsername String Internal identifier ❌ Never JWT tokens, internal APIs
userName String Public @handle ✅ Yes Profile URL, mentions, search, display

How it works:

Username Change Flow:

User changes userName from "alex" to "alexnew"
       │
       ▼
┌─────────────────────────────────┐
│ 1. Validate new userName        │
│ 2. Check availability           │
│ 3. Update userName in DB        │
│ 4. Return success               │
│                                 │
│ JWT stays valid (uses           │
│ systemUsername, unchanged)      │
│                                 │
│ NO LOGOUT REQUIRED ✅           │
└─────────────────────────────────┘

Database:

account_table:
  id              UUID PRIMARY KEY
  system_username VARCHAR(50) UNIQUE NOT NULL  -- "usr_550e8400e29b41d4"
  user_name       VARCHAR(30) UNIQUE NOT NULL  -- "alexvibes"
  ...

Flow Diagram

┌─────────────────────────────────────────────────────────────────┐
│                    SIGNUP FLOW (5 Screens)                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Screen 1: Sign Up Method                                       │
│  ┌─────────────────┐                                            │
│  │ Phone/Email/    │──► OTP Sent ──► Verify OTP ──► Account     │
│  │ Google/Apple    │                              Created       │
│  └─────────────────┘                                            │
│           │                                                     │
│           ▼                                                     │
│  Screen 2: Name & Birthdate                                     │
│  ┌─────────────────┐                                            │
│  │ Display Name    │──► Validate Age ──► Save                   │
│  │ Birthdate       │                                            │
│  └─────────────────┘                                            │
│           │                                                     │
│           ▼                                                     │
│  Screen 3: Profile Setup                                        │
│  ┌─────────────────┐                                            │
│  │ Profile Pic     │──► Username Check ──► Save                 │
│  │ Username        │                                            │
│  │ Bio             │                                            │
│  └─────────────────┘                                            │
│           │                                                     │
│           ▼                                                     │
│  Screen 4: Interests                                            │
│  ┌─────────────────┐                                            │
│  │ Select 5-10     │──► Save Preferences                        │
│  │ Categories      │                                            │
│  └─────────────────┘                                            │
│           │                                                     │
│           ▼                                                     │
│  Screen 5: Complete! ──► Home Feed                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                       LOGIN FLOW                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                    DEVICE CHECK FIRST                    │   │
│  │  ┌─────────────┐                                         │   │
│  │  │ Device Info │──► Known & Active? ──► YES ──► Continue │   │
│  │  └─────────────┘         │                               │   │
│  │                          NO                              │   │
│  │                          │                               │   │
│  │                          ▼                               │   │
│  │                    OTP Required First                    │   │
│  │                    (New/Inactive Device)                 │   │
│  └─────────────────────────────────────────────────────────┘   │
│                             │                                   │
│                             ▼                                   │
│  Option A: Password Login (if password set & device trusted)    │
│  ┌─────────────────┐                                            │
│  │ Identifier +    │──► Validate ──► Access Token               │
│  │ Password        │                                            │
│  └─────────────────┘                                            │
│        │                                                        │
│        └──► "Forgot Password?" ──► Password Reset Flow          │
│        └──► "Login with OTP instead" ──► Option B               │
│                                                                 │
│  Option B: Passwordless/OTP Login (always available)            │
│  ┌─────────────────┐                                            │
│  │ Phone/Email     │──► OTP Sent ──► Verify ──► Access Token    │
│  └─────────────────┘                                            │
│        │                                                        │
│        └──► "Lost access to phone/email?" ──► Account Recovery  │
│                                                                 │
│  Option C: Social Login (Google/Apple)                          │
│  ┌─────────────────┐                                            │
│  │ Google/Apple    │──► OAuth ──► Check Existing ──► Link/Create│
│  └─────────────────┘                      │                     │
│                                           ▼                     │
│                              ┌─────────────────────────┐        │
│                              │ Email matches existing? │        │
│                              │ YES → Link Account Flow │        │
│                              │ NO  → Create New        │        │
│                              └─────────────────────────┘        │
│                                                                 │
│  Account Recovery (Lost Access)                                 │
│  ┌─────────────────┐                                            │
│  │ Verify Identity │──► Support Ticket ──► Manual Review        │
│  │ (ID upload,     │                                            │
│  │  selfie, etc.)  │                                            │
│  └─────────────────┘                                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

SECURITY ARCHITECTURE

Why We Need Extra Protection for OAuth Users

The Problem:

Attacker hacks victim's Google account
           │
           ▼
Attacker can access ALL apps linked to that Google
           │
           ▼
😱 If we only rely on Google, attacker owns the account

Our Solution: Multi-Layer Security

┌─────────────────────────────────────────────────────────────────┐
│                    NEXTGATE SECURITY LAYERS                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Layer 1: Authentication (Who are you?)                         │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ Password / OTP / Google / Apple                          │   │
│  │ (Any of these can authenticate)                          │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  Layer 2: Device Trust (Is this your device?)                   │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ New device? → OTP to PHONE required                      │   │
│  │ Inactive 30+ days? → OTP to PHONE required               │   │
│  │ Trusted device? → Pass through                           │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  Layer 3: Sensitive Actions (Extra verification)                │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ Change password     → OTP to PHONE                       │   │
│  │ Change email        → OTP to PHONE                       │   │
│  │ Change phone        → OTP to OLD phone + NEW phone       │   │
│  │ Link/Unlink OAuth   → OTP to PHONE                       │   │
│  │ Delete account      → OTP to PHONE + Password (if set)   │   │
│  │ Large purchases     → OTP to PHONE (configurable)        │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  🔑 KEY INSIGHT: Phone is the ULTIMATE trust anchor             │
│     Even if Google/Apple/Email is hacked, phone protects you    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Phone as Ultimate Recovery Method

Auth Method Compromised Can Attacker Access Account?
Password leaked ❌ No - needs device OTP or phone OTP
Email hacked ❌ No - sensitive actions need phone OTP
Google hacked ❌ No - new device needs phone OTP
Apple hacked ❌ No - new device needs phone OTP
Phone stolen (unlocked) ⚠️ Partial - but needs password for sensitive actions
Phone + Password both ✅ Yes - full access (this is expected)

Mandatory Phone Verification

During onboarding, we STRONGLY encourage phone verification:

┌─────────────────────────────────────────────────────────────────┐
│                   ONBOARDING PHONE PROMPT                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  "Add your phone number for account security"                   │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 🔒 Recover your account if you lose access               │   │
│  │ 🔒 Get alerts about suspicious activity                  │   │
│  │ 🔒 Verify sensitive actions                              │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  [+255] [___________] [Verify]                                  │
│                                                                 │
│  [Skip for now] ← Show warning:                                 │
│  "Without a verified phone, you may lose access to your         │
│   account if you forget your password or lose access to         │
│   your Google/Apple account."                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

ACCOUNT LINKING & MERGING

When OAuth Email Matches Existing Account

Scenario Matrix:

Existing Account State OAuth Provider Action
Email verified + password Google (same email) Prompt to link
Email verified + no password Google (same email) Prompt to link
Email unverified Google (same email) Auto-link (Google verified it)
Phone only (no email set) Google (new email) Prompt to add email or create new
Email verified but different Google (different email) Create new account

POST /api/v1/auth/oauth/google

Request:

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

Response (Email Matches Existing - Link Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account found with this email. Please verify to link.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "LINK_REQUIRED",
    "existingAccount": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "maskedEmail": "a***@example.com",
      "maskedPhone": "+255*****678",
      "hasPassword": true,
      "authProviders": ["PHONE", "EMAIL"],
      "createdAt": "2024-06-15T10:00:00"
    },
    "linkOptions": [
      {
        "method": "PASSWORD",
        "available": true,
        "description": "Enter your password to link"
      },
      {
        "method": "OTP_PHONE",
        "available": true,
        "description": "Get OTP on +255*****678"
      },
      {
        "method": "OTP_EMAIL",
        "available": true,
        "description": "Get OTP on a***@example.com"
      }
    ],
    "oauthProvider": "GOOGLE",
    "oauthEmail": "alex@example.com",
    "tempToken": "eyJhbGciOiJIUzI1NiIs..."
  }
}

POST /api/v1/auth/account/link/confirm

Request:

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

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Google account linked successfully",
  "action_time": "2025-01-11T16:01:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "email": "alex@example.com",
      "authProviders": ["PHONE", "EMAIL", "GOOGLE"],
      "onboardingComplete": true
    },
    "linkedProvider": {
      "provider": "GOOGLE",
      "email": "alex@example.com",
      "linkedAt": "2025-01-11T16:01:00"
    }
  }
}

POST /api/v1/auth/account/link/request-otp

Request:

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

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP sent to your phone",
  "action_time": "2025-01-11T16:01:00",
  "data": {
    "otpToken": "eyJhbGciOiJIUzI1NiIs...",
    "sentTo": "+255*****678",
    "method": "SMS",
    "expiresAt": "2025-01-11T16:11:00"
  }
}

POST /api/v1/auth/account/link/confirm

Request:

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

When existing account has unverified email that matches Google email:

Response (Auto-Linked):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account linked and email verified",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "AUTO_LINKED",
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "email": "alex@example.com",
      "isEmailVerified": true,
      "authProviders": ["PHONE", "GOOGLE"]
    },
    "note": "Your Google account has been linked and email verified automatically"
  }
}

Create New Account (No Match)

Response (New Account):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account created successfully",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "CREATED",
    "isNewUser": true,
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "user": {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "systemUsername": "usr_660e8400e29b41d4",
      "userName": null,
      "email": "alex@example.com",
      "firstName": "Alex",
      "lastName": "Johnson",
      "profilePictureUrl": "https://lh3.googleusercontent.com/...",
      "isEmailVerified": true,
      "hasPassword": false,
      "authProviders": ["GOOGLE"],
      "onboardingStep": "NAME_BIRTHDATE",
      "onboardingComplete": false
    },
    "promptAddPhone": true,
    "phonePromptMessage": "Add your phone number to secure your account and enable recovery options"
  }
}

MANAGE LINKED ACCOUNTS

Get Linked Providers

GET /api/v1/auth/providers

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Linked providers retrieved",
  "action_time": "2025-01-11T18:00:00",
  "data": {
    "providers": [
      {
        "provider": "PHONE",
        "identifier": "+255*****678",
        "verified": true,
        "linkedAt": "2025-01-01T10:00:00",
        "isPrimary": true,
        "canUnlink": false,
        "unlinkBlockedReason": "Phone is your primary recovery method"
      },
      {
        "provider": "EMAIL",
        "identifier": "a***@example.com",
        "verified": true,
        "linkedAt": "2025-01-01T10:00:00",
        "isPrimary": false,
        "canUnlink": true
      },
      {
        "provider": "GOOGLE",
        "identifier": "a***@gmail.com",
        "verified": true,
        "linkedAt": "2025-01-11T16:01:00",
        "isPrimary": false,
        "canUnlink": true
      }
    ],
    "availableToLink": [
      {
        "provider": "APPLE",
        "description": "Sign in with Apple"
      }
    ],
    "hasPassword": true,
    "securityNote": "You have 3 ways to access your account"
  }
}

DELETE /api/v1/auth/providers/{provider}

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

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

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Google account unlinked",
  "action_time": "2025-01-11T18:10:00",
  "data": {
    "unlinkedProvider": "GOOGLE",
    "remainingProviders": ["PHONE", "EMAIL"],
    "securityNote": "You can no longer sign in with Google"
  }
}

Response (Cannot Unlink - Only Provider):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot unlink your only sign-in method",
  "action_time": "2025-01-11T18:10:00",
  "data": {
    "code": "CANNOT_UNLINK_ONLY_PROVIDER",
    "suggestion": "Add another sign-in method before unlinking this one"
  }
}

POST /api/v1/auth/providers/link

Request:

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

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Apple account linked successfully",
  "action_time": "2025-01-11T18:15:00",
  "data": {
    "linkedProvider": {
      "provider": "APPLE",
      "identifier": "a***@privaterelay.appleid.com",
      "linkedAt": "2025-01-11T18:15:00"
    },
    "totalProviders": 4
  }
}

ACCOUNT RECOVERY (Lost Access)

When User Can't Access Any Auth Method

POST /api/v1/auth/recovery/request

Request:

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

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Recovery request submitted",
  "action_time": "2025-01-11T20:00:00",
  "data": {
    "ticketId": "REC-2025-001234",
    "status": "PENDING_REVIEW",
    "estimatedResponseTime": "24-48 hours",
    "nextSteps": [
      "Check your backup email for instructions",
      "Prepare identity verification documents",
      "Our team will contact you within 48 hours"
    ],
    "requiredDocuments": [
      "Government-issued ID (passport, national ID)",
      "Selfie holding ID",
      "Proof of account ownership (screenshots, transaction history)"
    ]
  }
}

SENSITIVE ACTIONS - OTP VERIFICATION

All sensitive actions require OTP to phone (regardless of how user logged in):

Sensitive Actions List

Action OTP Required? Additional Verification
Change password ✅ Phone OTP Current password (if set)
Change email ✅ Phone OTP -
Change phone ✅ Old phone OTP + New phone OTP -
Link OAuth provider ✅ Phone OTP -
Unlink OAuth provider ✅ Phone OTP -
Delete account ✅ Phone OTP Password (if set)
View full payment methods ✅ Phone OTP -
Add payment method ❌ No -
Remove payment method ✅ Phone OTP -
Large purchase (>$100) ⚙️ Configurable -
Export account data ✅ Phone OTP -
Change security settings ✅ Phone OTP Password (if set)

Request OTP for Sensitive Action

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

Request:

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

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP sent to your phone",
  "action_time": "2025-01-11T19:00:00",
  "data": {
    "actionToken": "eyJhbGciOiJIUzI1NiIs...",
    "action": "CHANGE_EMAIL",
    "sentTo": "+255*****678",
    "method": "SMS",
    "expiresAt": "2025-01-11T19:10:00"
  }
}

Confirm Sensitive Action

POST /api/v1/auth/sensitive-action/confirm

Request:

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

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Email updated successfully",
  "action_time": "2025-01-11T19:01:00",
  "data": {
    "action": "CHANGE_EMAIL",
    "completed": true,
    "changes": {
      "previousEmail": "a***@example.com",
      "newEmail": "n***@example.com",
      "emailVerified": false
    },
    "note": "Please verify your new email address"
  }
}

NO PHONE? FALLBACK OPTIONS

If user didn't add phone during onboarding:

Prompt to Add Phone (Shown on sensitive actions)

{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Phone verification required for this action",
  "action_time": "2025-01-11T19:00:00",
  "data": {
    "code": "PHONE_REQUIRED",
    "action": "CHANGE_EMAIL",
    "options": [
      {
        "option": "ADD_PHONE",
        "description": "Add and verify your phone number first",
        "endpoint": "/api/v1/profile/add-phone"
      },
      {
        "option": "USE_PASSWORD",
        "available": true,
        "description": "Use your password instead (less secure)"
      },
      {
        "option": "CONTACT_SUPPORT",
        "description": "Contact support for manual verification"
      }
    ]
  }
}

SUMMARY: AUTH METHODS & SECURITY

Complete Auth Provider Matrix

Provider Can Signup? Can Login? Provides Email? Provides Phone? Trust Level
Phone + OTP HIGH
Email + OTP MEDIUM
Email + Password ❌ (need OTP first) MEDIUM
Google OAuth MEDIUM
Apple OAuth ✅ (may be relay) MEDIUM
Password only ✅ (trusted device) LOW

Security Recommendations for Users

{
  "securityScore": 75,
  "level": "GOOD",
  "recommendations": [
    {
      "priority": "HIGH",
      "action": "ADD_PHONE",
      "title": "Verify your phone number",
      "description": "Enables account recovery and secures sensitive actions",
      "completed": false
    },
    {
      "priority": "MEDIUM",
      "action": "SET_PASSWORD",
      "title": "Set a password",
      "description": "Faster login on trusted devices",
      "completed": true
    },
    {
      "priority": "LOW",
      "action": "LINK_BACKUP_EMAIL",
      "title": "Add backup email",
      "description": "Alternative recovery option",
      "completed": false
    }
  ]
}

---

## SCREEN 1: Sign Up

### 1.1 Initiate Signup (Phone)

**POST** `/api/v1/auth/signup/initiate`

**Request:**
```json
{
  "method": "PHONE",
  "phoneNumber": "+255712345678"
}

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP sent to your phone",
  "action_time": "2025-01-11T15:20:00",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "method": "PHONE",
    "maskedIdentifier": "+255*****678",
    "expiresAt": "2025-01-11T15:30:00",
    "resendAllowedAt": "2025-01-11T15:22:00",
    "attemptsRemaining": 3
  }
}

Response (Phone Already Registered):

{
  "success": false,
  "httpStatus": "CONFLICT",
  "message": "This phone number is already registered",
  "action_time": "2025-01-11T15:20:00",
  "data": {
    "code": "ACCOUNT_EXISTS",
    "field": "phoneNumber",
    "suggestion": "Please login instead"
  }
}

1.2 Initiate Signup (Email)

POST /api/v1/auth/signup/initiate

Request:

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

Response (Success):

{
  "success": true,
  "message": "OTP sent to your email",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "method": "EMAIL",
    "maskedIdentifier": "a***@example.com",
    "expiresAt": "2025-01-11T15:30:00Z",
    "resendAllowedAt": "2025-01-11T15:22:00Z",
    "attemptsRemaining": 3
  },
  "timestamp": "2025-01-11T15:20:00Z"
}

1.3 Verify Signup OTP

POST /api/v1/auth/signup/verify

Request:

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

Response (Success - Account Created):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account created successfully",
  "action_time": "2025-01-11T15:21:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": null,
      "phoneNumber": "+255712345678",
      "email": null,
      "isPhoneVerified": true,
      "isEmailVerified": false,
      "hasPassword": false,
      "onboardingStep": "NAME_BIRTHDATE",
      "onboardingComplete": false,
      "createdAt": "2025-01-11T15:20:00"
    }
  }
}

Response (Invalid OTP):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Invalid OTP code",
  "action_time": "2025-01-11T15:21:00",
  "data": {
    "code": "INVALID_OTP",
    "attemptsRemaining": 2
  }
}

Response (OTP Expired):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "OTP has expired",
  "action_time": "2025-01-11T15:35:00",
  "data": {
    "code": "OTP_EXPIRED",
    "suggestion": "Please request a new OTP"
  }
}

1.4 Resend OTP

POST /api/v1/auth/otp/resend

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs..."
}

Response (Success):

{
  "success": true,
  "message": "OTP resent successfully",
  "data": {
    "newTempToken": "eyJhbGciOiJIUzI1NiIs...",
    "maskedIdentifier": "+255*****678",
    "expiresAt": "2025-01-11T15:40:00Z",
    "resendAllowedAt": "2025-01-11T15:32:00Z",
    "attemptsRemaining": 2
  },
  "timestamp": "2025-01-11T15:30:00Z"
}

Response (Too Soon):

{
  "success": false,
  "message": "Please wait before requesting another OTP",
  "error": {
    "code": "RESEND_COOLDOWN",
    "resendAllowedAt": "2025-01-11T15:32:00Z",
    "waitSeconds": 90
  },
  "timestamp": "2025-01-11T15:30:30Z"
}

1.5 Social Signup (Google)

POST /api/v1/auth/signup/google

Request:

{
  "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..."
}

Response (Success - New User):

{
  "success": true,
  "message": "Account created successfully",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "isNewUser": true,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "email": "alex@gmail.com",
      "firstName": "Alex",
      "lastName": "Johnson",
      "profilePictureUrl": "https://lh3.googleusercontent.com/...",
      "isEmailVerified": true,
      "hasPassword": false,
      "authProvider": "GOOGLE",
      "onboardingStep": "NAME_BIRTHDATE",
      "onboardingComplete": false,
      "createdAt": "2025-01-11T15:20:00Z"
    }
  },
  "timestamp": "2025-01-11T15:20:00Z"
}

Response (Existing User - Login):

{
  "success": true,
  "message": "Welcome back!",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "isNewUser": false,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "userName": "alexj",
      "email": "alex@gmail.com",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "isEmailVerified": true,
      "hasPassword": true,
      "authProvider": "GOOGLE",
      "onboardingComplete": true
    }
  },
  "timestamp": "2025-01-11T15:20:00Z"
}

1.6 Social Signup (Apple)

POST /api/v1/auth/signup/apple

Request:

{
  "identityToken": "eyJraWQiOiJXNldjT0...",
  "authorizationCode": "c7e8a3f1b2d4...",
  "firstName": "Alex",
  "lastName": "Johnson"
}

Note: Apple only provides name on first authorization

Response: Same structure as Google response


SCREEN 2: Name & Birthdate

2.1 Save Name & Birthdate

PUT /api/v1/onboarding/name-birthdate

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

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

Response (Success):

{
  "success": true,
  "message": "Profile updated successfully",
  "data": {
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "displayName": "Alex Johnson",
      "firstName": "Alex",
      "lastName": "Johnson",
      "birthDate": "1995-06-15",
      "age": 29,
      "onboardingStep": "PROFILE_SETUP",
      "onboardingComplete": false
    }
  },
  "timestamp": "2025-01-11T15:22:00Z"
}

Response (Underage - Below 13):

{
  "success": false,
  "message": "You must be at least 13 years old to use this app",
  "error": {
    "code": "UNDERAGE",
    "field": "birthDate",
    "minimumAge": 13
  },
  "timestamp": "2025-01-11T15:22:00Z"
}

Response (Invalid Date):

{
  "success": false,
  "message": "Invalid birth date",
  "error": {
    "code": "INVALID_DATE",
    "field": "birthDate",
    "details": "Birth date cannot be in the future"
  },
  "timestamp": "2025-01-11T15:22:00Z"
}

SCREEN 3: Profile Setup

3.1 Check Username Availability

GET /api/v1/onboarding/username/check?username=alexvibes

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Note: Always returns suggestions (even when available) so user can pick alternatives

Response (Available):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Username is available",
  "action_time": "2025-01-11T15:23:00",
  "data": {
    "username": "alexvibes",
    "available": true,
    "valid": true,
    "suggestions": [
      "alexvibes_",
      "alexvibes1",
      "thealexvibes",
      "alexvibes_official",
      "real_alexvibes"
    ]
  }
}

Response (Taken):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Username is already taken",
  "action_time": "2025-01-11T15:23:00",
  "data": {
    "username": "alex",
    "available": false,
    "valid": true,
    "suggestions": [
      "alex123",
      "alex_vibes",
      "alexcool",
      "alex2025",
      "thealex"
    ]
  }
}

Response (Invalid Format):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Invalid username format",
  "action_time": "2025-01-11T15:23:00",
  "data": {
    "username": "123alex",
    "available": false,
    "valid": false,
    "validationError": "Username must start with a letter",
    "suggestions": [
      "alex123",
      "alex_user",
      "alexnew",
      "user_alex",
      "the_alex"
    ]
  }
}

3.2 Upload Profile Picture

POST /api/v1/onboarding/profile-picture

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: multipart/form-data

Request (Form Data):

file: [binary image data]

Response (Success):

{
  "success": true,
  "message": "Profile picture uploaded successfully",
  "data": {
    "profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380.jpg",
    "thumbnailUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380_thumb.jpg"
  },
  "timestamp": "2025-01-11T15:23:00Z"
}

Response (Invalid File):

{
  "success": false,
  "message": "Invalid file type",
  "error": {
    "code": "INVALID_FILE_TYPE",
    "allowedTypes": ["image/jpeg", "image/png", "image/webp"],
    "maxSizeMB": 5
  },
  "timestamp": "2025-01-11T15:23:00Z"
}

3.3 Save Profile Setup

PUT /api/v1/onboarding/profile-setup

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

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

Response (Success):

{
  "success": true,
  "message": "Profile setup completed",
  "data": {
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "bio": "Fashion lover | Shopaholic | Event creator 🔥",
      "profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380.jpg",
      "onboardingStep": "INTERESTS",
      "onboardingComplete": false
    }
  },
  "timestamp": "2025-01-11T15:24:00Z"
}

Response (Username Taken - Race Condition):

{
  "success": false,
  "message": "Username was just taken by another user",
  "error": {
    "code": "USERNAME_TAKEN",
    "field": "userName",
    "suggestions": [
      "alexvibes1",
      "alexvibes_",
      "thealexvibes"
    ]
  },
  "timestamp": "2025-01-11T15:24:00Z"
}

SCREEN 4: Interests

4.1 Get Available Interest Categories

GET /api/v1/onboarding/interests/categories

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "success": true,
  "message": "Categories retrieved successfully",
  "data": {
    "categories": [
      {
        "id": "cat_001",
        "name": "Fashion",
        "icon": "👗",
        "iconUrl": "https://cdn.example.com/icons/fashion.png",
        "color": "#FF6B6B"
      },
      {
        "id": "cat_002",
        "name": "Electronics",
        "icon": "📱",
        "iconUrl": "https://cdn.example.com/icons/electronics.png",
        "color": "#4ECDC4"
      },
      {
        "id": "cat_003",
        "name": "Beauty & Cosmetics",
        "icon": "💄",
        "iconUrl": "https://cdn.example.com/icons/beauty.png",
        "color": "#FF69B4"
      },
      {
        "id": "cat_004",
        "name": "Food & Drinks",
        "icon": "🍔",
        "iconUrl": "https://cdn.example.com/icons/food.png",
        "color": "#F39C12"
      },
      {
        "id": "cat_005",
        "name": "Sports & Fitness",
        "icon": "⚽",
        "iconUrl": "https://cdn.example.com/icons/sports.png",
        "color": "#2ECC71"
      },
      {
        "id": "cat_006",
        "name": "Music & Dance",
        "icon": "🎵",
        "iconUrl": "https://cdn.example.com/icons/music.png",
        "color": "#9B59B6"
      },
      {
        "id": "cat_007",
        "name": "Home & Decor",
        "icon": "🏠",
        "iconUrl": "https://cdn.example.com/icons/home.png",
        "color": "#E67E22"
      },
      {
        "id": "cat_008",
        "name": "Tech & Gadgets",
        "icon": "💻",
        "iconUrl": "https://cdn.example.com/icons/tech.png",
        "color": "#3498DB"
      },
      {
        "id": "cat_009",
        "name": "Travel",
        "icon": "✈️",
        "iconUrl": "https://cdn.example.com/icons/travel.png",
        "color": "#1ABC9C"
      },
      {
        "id": "cat_010",
        "name": "Gaming",
        "icon": "🎮",
        "iconUrl": "https://cdn.example.com/icons/gaming.png",
        "color": "#8E44AD"
      },
      {
        "id": "cat_011",
        "name": "Books & Reading",
        "icon": "📚",
        "iconUrl": "https://cdn.example.com/icons/books.png",
        "color": "#D35400"
      },
      {
        "id": "cat_012",
        "name": "Art & Design",
        "icon": "🎨",
        "iconUrl": "https://cdn.example.com/icons/art.png",
        "color": "#E74C3C"
      },
      {
        "id": "cat_013",
        "name": "Health & Wellness",
        "icon": "🧘",
        "iconUrl": "https://cdn.example.com/icons/wellness.png",
        "color": "#27AE60"
      },
      {
        "id": "cat_014",
        "name": "Automotive",
        "icon": "🚗",
        "iconUrl": "https://cdn.example.com/icons/auto.png",
        "color": "#34495E"
      },
      {
        "id": "cat_015",
        "name": "Pets & Animals",
        "icon": "🐾",
        "iconUrl": "https://cdn.example.com/icons/pets.png",
        "color": "#F1C40F"
      },
      {
        "id": "cat_016",
        "name": "Photography",
        "icon": "📷",
        "iconUrl": "https://cdn.example.com/icons/photo.png",
        "color": "#7F8C8D"
      },
      {
        "id": "cat_017",
        "name": "Kids & Baby",
        "icon": "👶",
        "iconUrl": "https://cdn.example.com/icons/kids.png",
        "color": "#FFB6C1"
      },
      {
        "id": "cat_018",
        "name": "Business & Finance",
        "icon": "💼",
        "iconUrl": "https://cdn.example.com/icons/business.png",
        "color": "#2C3E50"
      },
      {
        "id": "cat_019",
        "name": "Entertainment",
        "icon": "🎬",
        "iconUrl": "https://cdn.example.com/icons/entertainment.png",
        "color": "#C0392B"
      },
      {
        "id": "cat_020",
        "name": "DIY & Crafts",
        "icon": "🛠️",
        "iconUrl": "https://cdn.example.com/icons/diy.png",
        "color": "#16A085"
      }
    ],
    "minimumSelection": 3,
    "recommendedSelection": 5,
    "maximumSelection": 15
  },
  "timestamp": "2025-01-11T15:25:00Z"
}

4.2 Save User Interests

POST /api/v1/onboarding/interests

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

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

Response (Success):

{
  "success": true,
  "message": "Interests saved successfully",
  "data": {
    "selectedCount": 5,
    "interests": [
      { "id": "cat_001", "name": "Fashion" },
      { "id": "cat_003", "name": "Beauty & Cosmetics" },
      { "id": "cat_006", "name": "Music & Dance" },
      { "id": "cat_009", "name": "Travel" },
      { "id": "cat_012", "name": "Art & Design" }
    ],
    "onboardingStep": "COMPLETE",
    "onboardingComplete": true
  },
  "timestamp": "2025-01-11T15:26:00Z"
}

Response (Too Few Selected):

{
  "success": false,
  "message": "Please select at least 3 interests",
  "error": {
    "code": "MINIMUM_NOT_MET",
    "selectedCount": 1,
    "minimumRequired": 3
  },
  "timestamp": "2025-01-11T15:26:00Z"
}

4.3 Skip Interests (Optional)

POST /api/v1/onboarding/interests/skip

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

{}

Response:

{
  "success": true,
  "message": "Interests skipped. You can update them later in settings.",
  "data": {
    "onboardingStep": "COMPLETE",
    "onboardingComplete": true,
    "reminder": "We'll show you general content. Update your interests anytime for a personalized feed!"
  },
  "timestamp": "2025-01-11T15:26:00Z"
}

SCREEN 5: Complete Onboarding

5.1 Get Onboarding Summary & Suggestions

GET /api/v1/onboarding/complete

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "success": true,
  "message": "Welcome to the app! 🎉",
  "data": {
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic.jpg",
      "bio": "Fashion lover | Shopaholic | Event creator 🔥",
      "onboardingComplete": true,
      "hasPassword": false
    },
    "suggestions": {
      "accountsToFollow": [
        {
          "id": "user_001",
          "userName": "fashionista",
          "displayName": "Fashion Hub",
          "profilePictureUrl": "https://storage.example.com/...",
          "bio": "Latest fashion trends",
          "isVerified": true,
          "followerCount": 12500,
          "matchReason": "Based on your interest in Fashion"
        },
        {
          "id": "user_002",
          "userName": "beautyguru",
          "displayName": "Beauty Tips Daily",
          "profilePictureUrl": "https://storage.example.com/...",
          "bio": "Makeup tutorials & reviews",
          "isVerified": true,
          "followerCount": 8300,
          "matchReason": "Based on your interest in Beauty & Cosmetics"
        }
      ],
      "shopsToFollow": [
        {
          "id": "shop_001",
          "name": "Style Studio",
          "logoUrl": "https://storage.example.com/...",
          "category": "Fashion",
          "rating": 4.8,
          "productCount": 156,
          "matchReason": "Top rated in Fashion"
        }
      ],
      "upcomingEvents": [
        {
          "id": "event_001",
          "title": "Summer Fashion Show",
          "coverImageUrl": "https://storage.example.com/...",
          "startDate": "2025-01-20T18:00:00Z",
          "attendeeCount": 234,
          "matchReason": "Fashion event near you"
        }
      ]
    },
    "nextSteps": [
      {
        "action": "FOLLOW_ACCOUNTS",
        "title": "Follow 5 accounts",
        "description": "Get started by following accounts you like",
        "reward": null
      },
      {
        "action": "SET_PASSWORD",
        "title": "Set a password",
        "description": "For faster login next time",
        "reward": null
      },
      {
        "action": "FIRST_POST",
        "title": "Create your first post",
        "description": "Share something with the community",
        "reward": null
      }
    ]
  },
  "timestamp": "2025-01-11T15:27:00Z"
}

LOGIN FLOWS (With Device Trust)

Login Option A: Password Login

POST /api/v1/auth/login/password

Request:

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

identifier can be: username, email, or phone number

Response (Success - Trusted Device):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "requiresOtp": false,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "email": "alex@example.com",
      "phoneNumber": "+255712345678",
      "isEmailVerified": true,
      "isPhoneVerified": true,
      "hasPassword": true,
      "onboardingComplete": true
    },
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:00:00"
    }
  }
}

Response (New Device - OTP Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "New device detected. Please verify with OTP.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "requiresOtp": true,
    "otpReason": "NEW_DEVICE",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "otpSentTo": "+255*****678",
    "otpMethod": "SMS",
    "expiresAt": "2025-01-11T16:10:00",
    "device": {
      "deviceId": "xyz789-new-device",
      "deviceName": "Chrome on Windows",
      "isNew": true
    },
    "securityNote": "We detected a login from a new device. For your security, please verify with OTP."
  }
}

Response (Inactive Device 30+ days - OTP Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device inactive for 30+ days. Please verify with OTP.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "requiresOtp": true,
    "otpReason": "INACTIVE_DEVICE",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "otpSentTo": "+255*****678",
    "otpMethod": "SMS",
    "expiresAt": "2025-01-11T16:10:00",
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "lastActiveAt": "2024-12-01T10:00:00",
      "inactiveDays": 41
    },
    "securityNote": "This device hasn't been used in 41 days. Please verify it's you."
  }
}

Response (Wrong Password):

{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Invalid credentials",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "code": "INVALID_CREDENTIALS",
    "attemptsRemaining": 4,
    "suggestion": "Forgot password? Use OTP login instead"
  }
}

Response (Account Locked):

{
  "success": false,
  "httpStatus": "LOCKED",
  "message": "Account temporarily locked due to multiple failed attempts",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "code": "ACCOUNT_LOCKED",
    "unlockAt": "2025-01-11T16:30:00",
    "suggestion": "Try OTP login or wait 30 minutes"
  }
}

Response (No Password Set):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "No password set for this account",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "code": "NO_PASSWORD",
    "suggestion": "Use OTP login or social login"
  }
}

Login Option B: OTP Login (Passwordless)

B.1 Request OTP

POST /api/v1/auth/login/otp/request

Request:

{
  "identifier": "+255712345678",
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS",
    "appVersion": "1.2.0"
  }
}

identifier can be: email or phone number

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP sent to your phone",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "method": "SMS",
    "maskedIdentifier": "+255*****678",
    "expiresAt": "2025-01-11T16:10:00",
    "resendAllowedAt": "2025-01-11T16:02:00",
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "isNew": false,
      "lastActiveAt": "2025-01-10T14:30:00"
    }
  }
}

Response (User Not Found):

{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "No account found with this phone number",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "code": "USER_NOT_FOUND",
    "suggestion": "Would you like to create an account?",
    "createAccountUrl": "/api/v1/auth/signup/initiate"
  }
}

B.2 Verify OTP Login

POST /api/v1/auth/login/otp/verify

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456",
  "trustDevice": true,
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS",
    "appVersion": "1.2.0"
  }
}

Response (Success - Device Trusted):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2025-01-11T16:01:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "hasPassword": false,
      "onboardingComplete": true
    },
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:01:00"
    },
    "promptSetPassword": true,
    "passwordPromptMessage": "Set a password for faster login on trusted devices"
  }
}

Response (Success - Device NOT Trusted by user choice):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful (device not saved)",
  "action_time": "2025-01-11T16:01:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "hasPassword": false,
      "onboardingComplete": true
    },
    "device": {
      "trusted": false,
      "note": "This device will require OTP on next login"
    }
  }
}

Login Option C: Social Login (Google/Apple)

POST /api/v1/auth/oauth/google

Request:

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

Response (Existing User - Trusted Device):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Welcome back!",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "LOGIN",
    "isNewUser": false,
    "requiresOtp": false,
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "email": "alex@gmail.com",
      "profilePictureUrl": "https://storage.example.com/...",
      "isEmailVerified": true,
      "hasPassword": true,
      "authProviders": ["PHONE", "EMAIL", "GOOGLE"],
      "onboardingComplete": true
    },
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:00:00"
    }
  }
}

Response (Existing User - New Device - OTP Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "New device detected. Please verify with OTP.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "DEVICE_VERIFICATION_REQUIRED",
    "isNewUser": false,
    "requiresOtp": true,
    "otpReason": "NEW_DEVICE",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "otpSentTo": "+255*****678",
    "otpMethod": "SMS",
    "expiresAt": "2025-01-11T16:10:00",
    "user": {
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/..."
    },
    "device": {
      "deviceId": "xyz789-new-device",
      "deviceName": "Chrome on Windows",
      "isNew": true
    },
    "securityNote": "Even though you signed in with Google, we need to verify this new device."
  }
}

Response (Existing User - No Phone - Fallback Options):

{
  "success": true,
  "httpStatus": "OK",
  "message": "New device detected. Please verify to continue.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "VERIFICATION_REQUIRED_NO_PHONE",
    "isNewUser": false,
    "requiresVerification": true,
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "user": {
      "userName": "alexvibes",
      "displayName": "Alex Johnson"
    },
    "device": {
      "deviceId": "xyz789-new-device",
      "deviceName": "Chrome on Windows",
      "isNew": true
    },
    "verificationOptions": [
      {
        "method": "ADD_PHONE",
        "description": "Add phone number to receive OTP",
        "recommended": true
      },
      {
        "method": "OTP_EMAIL",
        "description": "Send OTP to a***@gmail.com",
        "available": true,
        "lessSecure": true,
        "note": "Email is less secure than phone"
      },
      {
        "method": "PASSWORD",
        "available": true,
        "description": "Enter your password"
      }
    ],
    "securityNote": "For better security, we recommend adding a phone number."
  }
}

Response (New User - Account Created):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account created successfully",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "CREATED",
    "isNewUser": true,
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "systemUsername": "usr_660e8400e29b41d4",
      "userName": null,
      "email": "alex@gmail.com",
      "firstName": "Alex",
      "lastName": "Johnson",
      "profilePictureUrl": "https://lh3.googleusercontent.com/...",
      "isEmailVerified": true,
      "hasPassword": false,
      "authProviders": ["GOOGLE"],
      "onboardingStep": "NAME_BIRTHDATE",
      "onboardingComplete": false
    },
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:00:00",
      "note": "First device is automatically trusted"
    },
    "promptAddPhone": true,
    "phonePromptMessage": "Add your phone number to secure your account"
  }
}

Response (Email Matches Existing - Link Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account found with this email. Please verify to link.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "LINK_REQUIRED",
    "isNewUser": false,
    "existingAccount": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "maskedEmail": "a***@example.com",
      "maskedPhone": "+255*****678",
      "hasPassword": true,
      "authProviders": ["PHONE", "EMAIL"],
      "createdAt": "2024-06-15T10:00:00"
    },
    "linkOptions": [
      {
        "method": "PASSWORD",
        "available": true,
        "description": "Enter your password to link"
      },
      {
        "method": "OTP_PHONE",
        "available": true,
        "description": "Get OTP on +255*****678"
      },
      {
        "method": "OTP_EMAIL",
        "available": true,
        "description": "Get OTP on a***@example.com"
      }
    ],
    "oauthProvider": "GOOGLE",
    "oauthEmail": "alex@gmail.com",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "note": "Device will be trusted after account link"
    }
  }
}

Apple Sign In

POST /api/v1/auth/oauth/apple

Request:

{
  "identityToken": "eyJraWQiOiJXNldjT0...",
  "authorizationCode": "c7e8a3f1b2d4...",
  "firstName": "Alex",
  "lastName": "Johnson",
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS",
    "appVersion": "1.2.0"
  }
}

Note: Apple only provides name on first authorization. Store it!

Responses: Same structure as Google OAuth responses above.


Verify Device OTP (After any login triggers device verification)

POST /api/v1/auth/login/verify-device

Request:

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

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device verified and trusted",
  "action_time": "2025-01-11T16:02:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "onboardingComplete": true
    },
    "device": {
      "deviceId": "xyz789-new-device",
      "deviceName": "Chrome on Windows",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:02:00"
    }
  }
}

POST-ONBOARDING: Set Password (Optional)

POST /api/v1/auth/password/set

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

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

Response (Success):

{
  "success": true,
  "message": "Password set successfully. You can now use password login.",
  "data": {
    "hasPassword": true,
    "passwordStrength": {
      "score": 85,
      "level": "STRONG",
      "feedback": "Great password!"
    }
  },
  "timestamp": "2025-01-11T16:05:00Z"
}

Response (Weak Password):

{
  "success": false,
  "message": "Password is too weak",
  "error": {
    "code": "WEAK_PASSWORD",
    "passwordStrength": {
      "score": 40,
      "level": "WEAK",
      "feedback": "Add uppercase letters and special characters"
    },
    "requirements": [
      "At least 8 characters",
      "At least one uppercase letter",
      "At least one lowercase letter",
      "At least one number",
      "At least one special character (@$!%*?&#)"
    ]
  },
  "timestamp": "2025-01-11T16:05:00Z"
}

TOKEN MANAGEMENT

Refresh Token

POST /api/v1/auth/token/refresh

Request:

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

Response (Success):

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

Response (Invalid/Expired Refresh Token):

{
  "success": false,
  "message": "Session expired. Please login again.",
  "error": {
    "code": "REFRESH_TOKEN_EXPIRED"
  },
  "timestamp": "2025-01-11T17:00:00Z"
}

Logout

POST /api/v1/auth/logout

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

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

Response:

{
  "success": true,
  "message": "Logged out successfully",
  "data": null,
  "timestamp": "2025-01-11T18:00:00Z"
}

PASSWORD RESET

Request Password Reset

POST /api/v1/auth/password/reset/request

Request:

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

Response:

{
  "success": true,
  "message": "Password reset OTP sent to your email",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "maskedIdentifier": "a***@example.com",
    "expiresAt": "2025-01-11T16:10:00Z"
  },
  "timestamp": "2025-01-11T16:00:00Z"
}

Verify Reset OTP & Set New Password

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

Request:

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

Response:

{
  "success": true,
  "message": "Password reset successfully. Please login with your new password.",
  "data": null,
  "timestamp": "2025-01-11T16:02:00Z"
}

GET CURRENT USER (Check Auth Status)

GET /api/v1/auth/me

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "User retrieved successfully",
  "action_time": "2025-01-11T18:00:00",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "systemUsername": "usr_550e8400e29b41d4",
    "userName": "alexvibes",
    "displayName": "Alex Johnson",
    "firstName": "Alex",
    "lastName": "Johnson",
    "email": "alex@example.com",
    "phoneNumber": "+255712345678",
    "profilePictureUrl": "https://storage.example.com/...",
    "bio": "Fashion lover | Shopaholic | Event creator 🔥",
    "birthDate": "1995-06-15",
    "isEmailVerified": true,
    "isPhoneVerified": true,
    "hasPassword": true,
    "authProviders": ["PHONE", "GOOGLE"],
    "onboardingComplete": true,
    "onboardingStep": "COMPLETE",
    "interests": [
      { "id": "cat_001", "name": "Fashion" },
      { "id": "cat_003", "name": "Beauty & Cosmetics" }
    ],
    "createdAt": "2025-01-11T15:20:00",
    "updatedAt": "2025-01-11T15:27:00"
  }
}

ONBOARDING STATUS CHECK

GET /api/v1/onboarding/status

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response (Incomplete):

{
  "success": true,
  "message": "Onboarding in progress",
  "data": {
    "onboardingComplete": false,
    "currentStep": "PROFILE_SETUP",
    "completedSteps": ["SIGNUP", "NAME_BIRTHDATE"],
    "remainingSteps": ["PROFILE_SETUP", "INTERESTS"],
    "progress": 50
  },
  "timestamp": "2025-01-11T15:23:00Z"
}

Response (Complete):

{
  "success": true,
  "message": "Onboarding complete",
  "data": {
    "onboardingComplete": true,
    "currentStep": "COMPLETE",
    "completedSteps": ["SIGNUP", "NAME_BIRTHDATE", "PROFILE_SETUP", "INTERESTS"],
    "remainingSteps": [],
    "progress": 100
  },
  "timestamp": "2025-01-11T15:27:00Z"
}

ERROR RESPONSE FORMAT (Standard)

All error responses follow GlobeFailureResponseBuilder structure:

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Human readable error message",
  "action_time": "2025-01-11T15:20:00",
  "data": {
    "code": "ERROR_CODE",
    "field": "fieldName",
    "details": "Additional details if any",
    "suggestion": "What user can do"
  }
}

Common Error Codes

Code HTTP Status Description
VALIDATION_ERROR BAD_REQUEST Request validation failed
INVALID_OTP BAD_REQUEST OTP code is incorrect
OTP_EXPIRED BAD_REQUEST OTP has expired
INVALID_CREDENTIALS UNAUTHORIZED Wrong password
TOKEN_EXPIRED UNAUTHORIZED Access token expired
REFRESH_TOKEN_EXPIRED UNAUTHORIZED Refresh token expired
UNAUTHORIZED UNAUTHORIZED Not authenticated
FORBIDDEN FORBIDDEN Not allowed
USER_NOT_FOUND NOT_FOUND User doesn't exist
ACCOUNT_EXISTS CONFLICT Account already exists
USERNAME_TAKEN CONFLICT Username is taken
PHONE_ALREADY_REGISTERED CONFLICT Phone belongs to another account
PHONE_TEMPORARILY_RESERVED CONFLICT Phone claimed but unverified
ACCOUNT_LOCKED LOCKED Too many failed attempts
RESEND_COOLDOWN TOO_MANY_REQUESTS Wait before resending OTP
RATE_LIMITED TOO_MANY_REQUESTS Too many requests
WEAK_PASSWORD UNPROCESSABLE_ENTITY Password doesn't meet requirements
UNDERAGE UNPROCESSABLE_ENTITY User is under minimum age
MIN_INTERESTS_REQUIRED UNPROCESSABLE_ENTITY Must have minimum interests
MAX_INTERESTS_REACHED UNPROCESSABLE_ENTITY Maximum interests reached
SERVER_ERROR INTERNAL_SERVER_ERROR Internal server error

ENUMS REFERENCE

OnboardingStep

SIGNUP
NAME_BIRTHDATE
PROFILE_SETUP
INTERESTS
COMPLETE

AuthProvider

PHONE
EMAIL
GOOGLE
APPLE

OTP Purpose

SIGNUP_VERIFICATION
LOGIN_OTP
PASSWORD_RESET
EMAIL_VERIFICATION
PHONE_VERIFICATION

SUMMARY: What's New vs Existing

New Endpoints

Modified Endpoints

Deprecated/Removed



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

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

Score Decay: 10% reduction per week of no interaction (keeps recommendations fresh)

Max Score: 1000 per category


ADMIN: Interest Category Management

Create Category

POST /api/v1/admin/interests/categories

Headers:

Authorization: Bearer {admin_token}

Request:

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

Response:

{
  "success": true,
  "message": "Category created successfully",
  "data": {
    "id": "cat_001",
    "name": "Fashion",
    "icon": "👗",
    "iconUrl": "https://cdn.example.com/icons/fashion.png",
    "color": "#FF6B6B",
    "keywords": ["clothes", "style", "outfit", "wear", "dress"],
    "displayOrder": 1,
    "isActive": true,
    "createdAt": "2025-01-11T16:00:00Z",
    "updatedAt": "2025-01-11T16:00:00Z"
  },
  "timestamp": "2025-01-11T16:00:00Z"
}

List All Categories (Admin View)

GET /api/v1/admin/interests/categories?includeInactive=true

Response:

{
  "success": true,
  "message": "Categories retrieved successfully",
  "data": {
    "categories": [
      {
        "id": "cat_001",
        "name": "Fashion",
        "icon": "👗",
        "iconUrl": "https://cdn.example.com/icons/fashion.png",
        "color": "#FF6B6B",
        "keywords": ["clothes", "style", "outfit"],
        "displayOrder": 1,
        "isActive": true,
        "stats": {
          "usersExplicit": 15420,
          "usersImplicit": 28750,
          "totalEngagements": 89500,
          "postsTagged": 8920,
          "productsTagged": 3450,
          "shopsTagged": 234,
          "eventsTagged": 89
        },
        "createdAt": "2025-01-01T10:00:00Z",
        "updatedAt": "2025-01-10T14:30:00Z"
      },
      {
        "id": "cat_002",
        "name": "Electronics",
        "icon": "📱",
        "iconUrl": "https://cdn.example.com/icons/electronics.png",
        "color": "#4ECDC4",
        "keywords": ["phone", "laptop", "gadget", "tech"],
        "displayOrder": 2,
        "isActive": true,
        "stats": {
          "usersExplicit": 12300,
          "usersImplicit": 31000,
          "totalEngagements": 125000,
          "postsTagged": 5600,
          "productsTagged": 8900,
          "shopsTagged": 456,
          "eventsTagged": 23
        },
        "createdAt": "2025-01-01T10:00:00Z",
        "updatedAt": "2025-01-10T14:30:00Z"
      }
    ],
    "totalCategories": 20,
    "activeCategories": 18,
    "inactiveCategories": 2
  },
  "timestamp": "2025-01-11T16:00:00Z"
}

Update Category

PUT /api/v1/admin/interests/categories/{categoryId}

Request:

{
  "name": "Fashion & Style",
  "icon": "👗",
  "iconUrl": "https://cdn.example.com/icons/fashion-v2.png",
  "color": "#FF5252",
  "keywords": ["clothes", "style", "outfit", "wear", "dress", "apparel"],
  "isActive": true
}

Response:

{
  "success": true,
  "message": "Category updated successfully",
  "data": {
    "id": "cat_001",
    "name": "Fashion & Style",
    "icon": "👗",
    "iconUrl": "https://cdn.example.com/icons/fashion-v2.png",
    "color": "#FF5252",
    "keywords": ["clothes", "style", "outfit", "wear", "dress", "apparel"],
    "displayOrder": 1,
    "isActive": true,
    "updatedAt": "2025-01-11T16:05:00Z"
  },
  "timestamp": "2025-01-11T16:05:00Z"
}

Reorder Categories

PUT /api/v1/admin/interests/categories/reorder

Request:

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

Response:

{
  "success": true,
  "message": "Categories reordered successfully",
  "data": {
    "updated": 3
  },
  "timestamp": "2025-01-11T16:10:00Z"
}

Deactivate Category (Soft Delete)

DELETE /api/v1/admin/interests/categories/{categoryId}

Response:

{
  "success": true,
  "message": "Category deactivated successfully",
  "data": {
    "id": "cat_015",
    "name": "Outdated Category",
    "isActive": false,
    "deactivatedAt": "2025-01-11T16:15:00Z",
    "note": "Category hidden from users. Existing user data preserved."
  },
  "timestamp": "2025-01-11T16:15:00Z"
}

Get Category Analytics

GET /api/v1/admin/interests/categories/{categoryId}/analytics?period=30d

Response:

{
  "success": true,
  "message": "Analytics retrieved successfully",
  "data": {
    "categoryId": "cat_001",
    "categoryName": "Fashion",
    "period": "LAST_30_DAYS",
    "overview": {
      "totalUsers": 44170,
      "explicitUsers": 15420,
      "implicitUsers": 28750,
      "averageScore": 67.5,
      "totalEngagements": 89500
    },
    "trend": {
      "direction": "UP",
      "percentageChange": 12.5,
      "newUsersThisPeriod": 2340
    },
    "engagement": {
      "likes": 34000,
      "comments": 12500,
      "shares": 8900,
      "saves": 15600,
      "purchases": 3200
    },
    "topContent": {
      "topPosts": [
        { "postId": "post_123", "engagements": 4500 },
        { "postId": "post_456", "engagements": 3200 }
      ],
      "topProducts": [
        { "productId": "prod_789", "sales": 234 },
        { "productId": "prod_012", "sales": 189 }
      ]
    },
    "demographics": {
      "ageGroups": [
        { "range": "13-17", "percentage": 15 },
        { "range": "18-24", "percentage": 35 },
        { "range": "25-34", "percentage": 30 },
        { "range": "35-44", "percentage": 12 },
        { "range": "45+", "percentage": 8 }
      ]
    }
  },
  "timestamp": "2025-01-11T16:20:00Z"
}

PUBLIC: Interest Categories

Get Active Categories (Onboarding & Settings)

GET /api/v1/interests/categories

No auth required for onboarding, shows only active categories

Response:

{
  "success": true,
  "message": "Categories retrieved successfully",
  "data": {
    "categories": [
      {
        "id": "cat_001",
        "name": "Fashion",
        "icon": "👗",
        "iconUrl": "https://cdn.example.com/icons/fashion.png",
        "color": "#FF6B6B"
      },
      {
        "id": "cat_002",
        "name": "Electronics",
        "icon": "📱",
        "iconUrl": "https://cdn.example.com/icons/electronics.png",
        "color": "#4ECDC4"
      },
      {
        "id": "cat_003",
        "name": "Beauty & Cosmetics",
        "icon": "💄",
        "iconUrl": "https://cdn.example.com/icons/beauty.png",
        "color": "#FF69B4"
      },
      {
        "id": "cat_004",
        "name": "Food & Drinks",
        "icon": "🍔",
        "iconUrl": "https://cdn.example.com/icons/food.png",
        "color": "#F39C12"
      },
      {
        "id": "cat_005",
        "name": "Sports & Fitness",
        "icon": "⚽",
        "iconUrl": "https://cdn.example.com/icons/sports.png",
        "color": "#2ECC71"
      },
      {
        "id": "cat_006",
        "name": "Music & Dance",
        "icon": "🎵",
        "iconUrl": "https://cdn.example.com/icons/music.png",
        "color": "#9B59B6"
      },
      {
        "id": "cat_007",
        "name": "Home & Decor",
        "icon": "🏠",
        "iconUrl": "https://cdn.example.com/icons/home.png",
        "color": "#E67E22"
      },
      {
        "id": "cat_008",
        "name": "Tech & Gadgets",
        "icon": "💻",
        "iconUrl": "https://cdn.example.com/icons/tech.png",
        "color": "#3498DB"
      },
      {
        "id": "cat_009",
        "name": "Travel",
        "icon": "✈️",
        "iconUrl": "https://cdn.example.com/icons/travel.png",
        "color": "#1ABC9C"
      },
      {
        "id": "cat_010",
        "name": "Gaming",
        "icon": "🎮",
        "iconUrl": "https://cdn.example.com/icons/gaming.png",
        "color": "#8E44AD"
      },
      {
        "id": "cat_011",
        "name": "Books & Reading",
        "icon": "📚",
        "iconUrl": "https://cdn.example.com/icons/books.png",
        "color": "#D35400"
      },
      {
        "id": "cat_012",
        "name": "Art & Design",
        "icon": "🎨",
        "iconUrl": "https://cdn.example.com/icons/art.png",
        "color": "#E74C3C"
      },
      {
        "id": "cat_013",
        "name": "Health & Wellness",
        "icon": "🧘",
        "iconUrl": "https://cdn.example.com/icons/wellness.png",
        "color": "#27AE60"
      },
      {
        "id": "cat_014",
        "name": "Automotive",
        "icon": "🚗",
        "iconUrl": "https://cdn.example.com/icons/auto.png",
        "color": "#34495E"
      },
      {
        "id": "cat_015",
        "name": "Pets & Animals",
        "icon": "🐾",
        "iconUrl": "https://cdn.example.com/icons/pets.png",
        "color": "#F1C40F"
      },
      {
        "id": "cat_016",
        "name": "Photography",
        "icon": "📷",
        "iconUrl": "https://cdn.example.com/icons/photo.png",
        "color": "#7F8C8D"
      },
      {
        "id": "cat_017",
        "name": "Kids & Baby",
        "icon": "👶",
        "iconUrl": "https://cdn.example.com/icons/kids.png",
        "color": "#FFB6C1"
      },
      {
        "id": "cat_018",
        "name": "Business & Finance",
        "icon": "💼",
        "iconUrl": "https://cdn.example.com/icons/business.png",
        "color": "#2C3E50"
      },
      {
        "id": "cat_019",
        "name": "Entertainment",
        "icon": "🎬",
        "iconUrl": "https://cdn.example.com/icons/entertainment.png",
        "color": "#C0392B"
      },
      {
        "id": "cat_020",
        "name": "DIY & Crafts",
        "icon": "🛠️",
        "iconUrl": "https://cdn.example.com/icons/diy.png",
        "color": "#16A085"
      }
    ],
    "selectionRules": {
      "minimum": 3,
      "recommended": 5,
      "maximum": 15,
      "canSkip": true
    }
  },
  "timestamp": "2025-01-11T15:25:00Z"
}

USER: Interest Management

Get My Interests

GET /api/v1/interests/me

Headers:

Authorization: Bearer {access_token}

Response:

{
  "success": true,
  "message": "Interests retrieved successfully",
  "data": {
    "explicit": [
      {
        "categoryId": "cat_001",
        "categoryName": "Fashion",
        "icon": "👗",
        "color": "#FF6B6B",
        "source": "ONBOARDING",
        "addedAt": "2025-01-11T15:26:00Z"
      },
      {
        "categoryId": "cat_003",
        "categoryName": "Beauty & Cosmetics",
        "icon": "💄",
        "color": "#FF69B4",
        "source": "SETTINGS",
        "addedAt": "2025-01-12T10:00:00Z"
      }
    ],
    "topImplicit": [
      {
        "categoryId": "cat_002",
        "categoryName": "Electronics",
        "icon": "📱",
        "color": "#4ECDC4",
        "score": 87,
        "trend": "RISING"
      },
      {
        "categoryId": "cat_006",
        "categoryName": "Music & Dance",
        "icon": "🎵",
        "color": "#9B59B6",
        "score": 65,
        "trend": "STABLE"
      },
      {
        "categoryId": "cat_009",
        "categoryName": "Travel",
        "icon": "✈️",
        "color": "#1ABC9C",
        "score": 42,
        "trend": "FALLING"
      }
    ],
    "summary": {
      "explicitCount": 2,
      "implicitCount": 8,
      "topCategory": "Fashion",
      "feedPersonalization": "HIGH"
    }
  },
  "timestamp": "2025-01-11T18:00:00Z"
}

Update My Explicit Interests

PUT /api/v1/interests/me

Headers:

Authorization: Bearer {access_token}

Request:

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

Response:

{
  "success": true,
  "message": "Interests updated successfully",
  "data": {
    "explicit": [
      { "categoryId": "cat_001", "categoryName": "Fashion" },
      { "categoryId": "cat_003", "categoryName": "Beauty & Cosmetics" },
      { "categoryId": "cat_006", "categoryName": "Music & Dance" },
      { "categoryId": "cat_010", "categoryName": "Gaming" },
      { "categoryId": "cat_012", "categoryName": "Art & Design" }
    ],
    "changes": {
      "added": ["cat_006", "cat_010", "cat_012"],
      "removed": [],
      "unchanged": ["cat_001", "cat_003"]
    },
    "feedImpact": "Your feed will now show more Music, Gaming, and Art content"
  },
  "timestamp": "2025-01-11T18:05:00Z"
}

Add Single Interest

POST /api/v1/interests/me/{categoryId}

Headers:

Authorization: Bearer {access_token}

Response:

{
  "success": true,
  "message": "Interest added successfully",
  "data": {
    "categoryId": "cat_015",
    "categoryName": "Pets & Animals",
    "icon": "🐾",
    "addedAt": "2025-01-11T18:10:00Z",
    "totalExplicit": 6
  },
  "timestamp": "2025-01-11T18:10:00Z"
}

Response (Max Reached):

{
  "success": false,
  "message": "Maximum interests reached",
  "error": {
    "code": "MAX_INTERESTS_REACHED",
    "current": 15,
    "maximum": 15,
    "suggestion": "Remove an interest before adding a new one"
  },
  "timestamp": "2025-01-11T18:10:00Z"
}

Remove Single Interest

DELETE /api/v1/interests/me/{categoryId}

Headers:

Authorization: Bearer {access_token}

Response:

{
  "success": true,
  "message": "Interest removed successfully",
  "data": {
    "categoryId": "cat_015",
    "categoryName": "Pets & Animals",
    "removedAt": "2025-01-11T18:15:00Z",
    "totalExplicit": 5,
    "note": "You may still see some Pets & Animals content based on your activity"
  },
  "timestamp": "2025-01-11T18:15:00Z"
}

Response (Minimum Required):

{
  "success": false,
  "message": "Cannot remove interest",
  "error": {
    "code": "MIN_INTERESTS_REQUIRED",
    "current": 3,
    "minimum": 3,
    "suggestion": "Add another interest before removing this one"
  },
  "timestamp": "2025-01-11T18:15:00Z"
}

Hide Content Category (Negative Signal)

POST /api/v1/interests/me/{categoryId}/hide

User explicitly says "not interested" - strong negative signal

Headers:

Authorization: Bearer {access_token}

Response:

{
  "success": true,
  "message": "You'll see less of this content",
  "data": {
    "categoryId": "cat_018",
    "categoryName": "Business & Finance",
    "action": "HIDDEN",
    "feedImpact": "Business & Finance content will be significantly reduced in your feed",
    "canUndo": true,
    "undoExpiry": "2025-01-11T18:30:00Z"
  },
  "timestamp": "2025-01-11T18:20:00Z"
}

Unhide Content Category

DELETE /api/v1/interests/me/{categoryId}/hide

Response:

{
  "success": true,
  "message": "Category unhidden",
  "data": {
    "categoryId": "cat_018",
    "categoryName": "Business & Finance",
    "action": "UNHIDDEN",
    "feedImpact": "Business & Finance content will appear normally based on your activity"
  },
  "timestamp": "2025-01-11T18:25:00Z"
}

Get Hidden Categories

GET /api/v1/interests/me/hidden

Response:

{
  "success": true,
  "message": "Hidden categories retrieved",
  "data": {
    "hidden": [
      {
        "categoryId": "cat_018",
        "categoryName": "Business & Finance",
        "icon": "💼",
        "hiddenAt": "2025-01-11T18:20:00Z"
      },
      {
        "categoryId": "cat_014",
        "categoryName": "Automotive",
        "icon": "🚗",
        "hiddenAt": "2025-01-05T12:00:00Z"
      }
    ],
    "totalHidden": 2
  },
  "timestamp": "2025-01-11T18:30:00Z"
}

INTERNAL: Silent Interest Tracking

These are NOT public API endpoints. They are triggered internally when users interact with content.

How It Works

When user performs any action on content (post, product, shop, event), the system:

  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

User Action System Tracks
Views post POST_VIEW on post's categories
Likes post POST_LIKE on post's categories
Comments on post POST_COMMENT on post's categories
Shares post POST_SHARE on post's categories
Saves post POST_SAVE on post's categories
Views product PRODUCT_VIEW on product's category
Adds to cart PRODUCT_CART on product's category
Purchases PRODUCT_PURCHASE on product's category
Views shop SHOP_VIEW on shop's categories
Follows shop SHOP_FOLLOW on shop's categories
Views event EVENT_VIEW on event's categories
RSVPs to event EVENT_RSVP on event's categories
Follows user USER_FOLLOW on user's primary categories
Searches SEARCH on matched categories
Scrolls past quickly SCROLL_PAST (negative)
Clicks "Not interested" HIDE_CONTENT (strong negative)

Content Must Have Category Tags

For tracking to work, all content must be tagged:

// Post
{
  "id": "post_123",
  "content": "Check out my new outfit!",
  "categoryIds": ["cat_001"]  // Fashion
}

// Product
{
  "id": "prod_456", 
  "name": "Wireless Earbuds",
  "categoryId": "cat_002"  // Electronics
}

// Shop
{
  "id": "shop_789",
  "name": "StyleHub",
  "categoryIds": ["cat_001", "cat_003"]  // Fashion, Beauty
}

// Event
{
  "id": "event_012",
  "title": "Summer Music Festival",
  "categoryIds": ["cat_006", "cat_019"]  // Music, Entertainment
}

FEED: Using Interests for Recommendations

Get Personalized Feed

GET /api/v1/feed?page=1&size=20

Feed algorithm uses interest scores to rank content

Response includes personalization info:

{
  "success": true,
  "data": {
    "posts": [
      {
        "id": "post_123",
        "content": "...",
        "author": { "...": "..." },
        "categoryIds": ["cat_001"],
        "relevanceScore": 0.95,
        "relevanceReason": "EXPLICIT_INTEREST"
      },
      {
        "id": "post_456",
        "content": "...",
        "author": { "...": "..." },
        "categoryIds": ["cat_002"],
        "relevanceScore": 0.87,
        "relevanceReason": "HIGH_IMPLICIT_SCORE"
      },
      {
        "id": "post_789",
        "content": "...",
        "author": { "...": "..." },
        "categoryIds": ["cat_015"],
        "relevanceScore": 0.45,
        "relevanceReason": "TRENDING"
      }
    ],
    "personalization": {
      "status": "ACTIVE",
      "basedOn": {
        "explicitInterests": 5,
        "implicitInterests": 8,
        "followedAccounts": 23
      }
    },
    "pagination": { "...": "..." }
  }
}

SUMMARY: Interest System Endpoints

Admin Endpoints

Method Endpoint Description
POST /api/v1/admin/interests/categories Create category
GET /api/v1/admin/interests/categories List all (with stats)
PUT /api/v1/admin/interests/categories/{id} Update category
DELETE /api/v1/admin/interests/categories/{id} Deactivate category
PUT /api/v1/admin/interests/categories/reorder Reorder categories
GET /api/v1/admin/interests/categories/{id}/analytics Get analytics

Public Endpoints

Method Endpoint Description
GET /api/v1/interests/categories Get active categories

User Endpoints (Requires Auth)

Method Endpoint Description
GET /api/v1/interests/me Get my interests
PUT /api/v1/interests/me Update all explicit
POST /api/v1/interests/me/{id} Add single interest
DELETE /api/v1/interests/me/{id} Remove single interest
POST /api/v1/interests/me/{id}/hide Hide category
DELETE /api/v1/interests/me/{id}/hide Unhide category
GET /api/v1/interests/me/hidden Get hidden list

Internal (No Public API)


New Database Fields Needed

AccountEntity

// Existing fields to keep
private UUID id;
private String userName;              // Public @handle (changeable)
private String phoneNumber;
private String email;
private String password;
private String firstName;
private String lastName;
private String middleName;
private String bio;
private String location;
private Boolean isVerified;
private Boolean isEmailVerified;
private Boolean isPhoneVerified;
private boolean twoFactorEnabled;
private String twoFactorSecret;
private boolean locked;
private String lockedReason;
private LocalDateTime createdAt;
private LocalDateTime editedAt;
private Set<Roles> 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:

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

Refresh Token Payload:

{
  "sub": "usr_550e8400e29b41d4",
  "tokenType": "REFRESH",
  "iat": 1736605200,
  "exp": 1768141200
}

Note: sub (subject) uses systemUsername, NOT userName. This allows username changes without token invalidation.


Username Change Flow (No Logout Required)

PUT /api/v1/profile/update-basic-info

Request:

{
  "userName": "newusername"
}

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Username updated successfully",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "systemUsername": "usr_550e8400e29b41d4",
    "userName": "newusername",
    "previousUserName": "oldusername",
    "note": "Your profile URL is now: app.com/@newusername"
  }
}

JWT token remains valid because it uses systemUsername which hasn't changed.


System Username Generation

Generated automatically at account creation:

public String generateSystemUsername(UUID userId) {
    // Take first 16 chars of UUID (without hyphens)
    String shortId = userId.toString().replace("-", "").substring(0, 16);
    return "usr_" + shortId;
}

// Example:
// UUID: 550e8400-e29b-41d4-a716-446655440000
// systemUsername: usr_550e8400e29b41d4

Rules:


DEVICE TRUST & LOGIN SECURITY

Overview

The system tracks user devices and applies a sliding trust window:

Trust Sliding Window

Device Activity Timeline:
─────────────────────────────────────────────────────────────────►
                                                              NOW
     │                    │                    │
     ▼                    ▼                    ▼
  Day 0               Day 25               Day 35
  (Login)            (Last use)           (Login attempt)
     │                    │                    │
     │◄────── TRUSTED ────►│                   │
     │                    │◄─── 30 DAY GAP ───►│
     │                                         │
     │                              Device now UNTRUSTED
     │                              OTP required to re-trust

Login Decision Matrix

Device Status Last Activity Password Set? Action
New (unknown) Never Yes OTP first → then password → trust device
New (unknown) Never No OTP only → trust device
Known trusted < 30 days Yes Password only ✅
Known trusted < 30 days No Auto-login or OTP
Known trusted > 30 days Yes OTP first → then password → re-trust
Known trusted > 30 days No OTP → re-trust
Known untrusted Any Any OTP required
Any (suspicious) Any Any OTP required + security alert

Suspicious Activity Triggers


LOGIN FLOWS (Updated with Device Trust)

Password Login - Full Flow

POST /api/v1/auth/login/password

Request:

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

Response (Success - Trusted Device):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "requiresOtp": false,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "onboardingComplete": true
    },
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:00:00"
    }
  }
}

Response (New/Untrusted Device - OTP Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "New device detected. Please verify with OTP.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "requiresOtp": true,
    "otpReason": "NEW_DEVICE",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "otpSentTo": "+255*****678",
    "otpMethod": "SMS",
    "expiresAt": "2025-01-11T16:10:00",
    "device": {
      "deviceId": "xyz789-new-device",
      "deviceName": "Chrome on Windows",
      "isNew": true
    },
    "securityNote": "We detected a login from a new device. For your security, please verify with OTP."
  }
}

Response (Inactive Device - OTP Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device inactive for 30+ days. Please verify with OTP.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "requiresOtp": true,
    "otpReason": "INACTIVE_DEVICE",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "otpSentTo": "a***@example.com",
    "otpMethod": "EMAIL",
    "expiresAt": "2025-01-11T16:10:00",
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "lastActiveAt": "2024-12-01T10:00:00",
      "inactiveDays": 41
    },
    "securityNote": "This device hasn't been used in 41 days. Please verify it's you."
  }
}

Verify Device OTP (Step 2 for untrusted devices)

POST /api/v1/auth/login/verify-device

Request:

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

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device verified and trusted",
  "action_time": "2025-01-11T16:02:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "onboardingComplete": true
    },
    "device": {
      "deviceId": "xyz789-new-device",
      "deviceName": "Chrome on Windows",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:02:00"
    }
  }
}

Login Without Trusting Device (One-time access)

Request:

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

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful (device not saved)",
  "action_time": "2025-01-11T16:02:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": { ... },
    "device": {
      "trusted": false,
      "note": "This device will require OTP on next login"
    }
  }
}

DEVICE MANAGEMENT

Get My Devices

GET /api/v1/auth/devices

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Devices retrieved successfully",
  "action_time": "2025-01-11T18:00:00",
  "data": {
    "devices": [
      {
        "id": "device-uuid-1",
        "deviceId": "abc123-device-fingerprint",
        "deviceName": "iPhone 15 Pro",
        "deviceType": "MOBILE_IOS",
        "isTrusted": true,
        "isCurrentDevice": true,
        "lastActiveAt": "2025-01-11T18:00:00",
        "lastIpAddress": "196.41.xxx.xxx",
        "lastLocation": "Dar es Salaam, Tanzania",
        "createdAt": "2025-01-01T10:00:00"
      },
      {
        "id": "device-uuid-2",
        "deviceId": "xyz789-device-fingerprint",
        "deviceName": "Chrome on MacBook",
        "deviceType": "WEB_BROWSER",
        "isTrusted": true,
        "isCurrentDevice": false,
        "lastActiveAt": "2025-01-10T14:30:00",
        "lastIpAddress": "196.41.xxx.xxx",
        "lastLocation": "Dar es Salaam, Tanzania",
        "createdAt": "2024-12-15T08:00:00"
      },
      {
        "id": "device-uuid-3",
        "deviceId": "old-device-fingerprint",
        "deviceName": "Samsung Galaxy S21",
        "deviceType": "MOBILE_ANDROID",
        "isTrusted": false,
        "isCurrentDevice": false,
        "lastActiveAt": "2024-11-20T09:00:00",
        "lastIpAddress": "41.59.xxx.xxx",
        "lastLocation": "Arusha, Tanzania",
        "createdAt": "2024-06-01T12:00:00",
        "untrustedReason": "Inactive for 52 days"
      }
    ],
    "totalDevices": 3,
    "trustedDevices": 2
  }
}

Remove/Logout Device

DELETE /api/v1/auth/devices/{deviceId}

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device removed successfully",
  "action_time": "2025-01-11T18:05:00",
  "data": {
    "removedDeviceId": "device-uuid-3",
    "removedDeviceName": "Samsung Galaxy S21",
    "note": "This device has been logged out and will require OTP to login again"
  }
}

Logout All Other Devices

POST /api/v1/auth/devices/logout-all

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

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

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "All other devices logged out",
  "action_time": "2025-01-11T18:10:00",
  "data": {
    "devicesLoggedOut": 2,
    "currentDeviceKept": true,
    "note": "All other devices will require OTP to login again"
  }
}

Rename Device

PUT /api/v1/auth/devices/{deviceId}

Request:

{
  "deviceName": "My Work Laptop"
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device renamed successfully",
  "action_time": "2025-01-11T18:15:00",
  "data": {
    "deviceId": "device-uuid-2",
    "deviceName": "My Work Laptop",
    "previousName": "Chrome on MacBook"
  }
}

LOGIN EVENTS/HISTORY

Get Login History

GET /api/v1/auth/login-history?page=1&size=20

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login history retrieved",
  "action_time": "2025-01-11T18:20:00",
  "data": {
    "events": [
      {
        "id": "event-uuid-1",
        "loginMethod": "PASSWORD",
        "deviceName": "iPhone 15 Pro",
        "deviceType": "MOBILE_IOS",
        "ipAddress": "196.41.xxx.xxx",
        "location": "Dar es Salaam, Tanzania",
        "status": "SUCCESS",
        "requiresOtp": false,
        "createdAt": "2025-01-11T16:00:00"
      },
      {
        "id": "event-uuid-2",
        "loginMethod": "OTP",
        "deviceName": "Chrome on Windows",
        "deviceType": "WEB_BROWSER",
        "ipAddress": "196.41.xxx.xxx",
        "location": "Dar es Salaam, Tanzania",
        "status": "SUCCESS",
        "requiresOtp": true,
        "otpReason": "NEW_DEVICE",
        "createdAt": "2025-01-11T14:30:00"
      },
      {
        "id": "event-uuid-3",
        "loginMethod": "PASSWORD",
        "deviceName": "Unknown Device",
        "deviceType": "WEB_BROWSER",
        "ipAddress": "102.89.xxx.xxx",
        "location": "Lagos, Nigeria",
        "status": "FAILED_PASSWORD",
        "requiresOtp": false,
        "createdAt": "2025-01-10T23:45:00",
        "securityAlert": true
      }
    ],
    "pagination": {
      "page": 1,
      "size": 20,
      "totalElements": 45,
      "totalPages": 3
    },
    "securitySummary": {
      "totalLogins30Days": 28,
      "failedAttempts30Days": 2,
      "uniqueDevices30Days": 3,
      "uniqueLocations30Days": 1
    }
  }
}

SECURITY ALERTS

Get Security Alerts

GET /api/v1/auth/security-alerts

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Security alerts retrieved",
  "action_time": "2025-01-11T18:25:00",
  "data": {
    "alerts": [
      {
        "id": "alert-uuid-1",
        "type": "SUSPICIOUS_LOGIN",
        "severity": "HIGH",
        "title": "Login attempt from new location",
        "description": "Someone tried to login from Lagos, Nigeria",
        "deviceName": "Unknown Device",
        "ipAddress": "102.89.xxx.xxx",
        "location": "Lagos, Nigeria",
        "status": "UNREAD",
        "actionTaken": "BLOCKED",
        "createdAt": "2025-01-10T23:45:00",
        "actions": [
          { "action": "DISMISS", "label": "This was me" },
          { "action": "SECURE_ACCOUNT", "label": "Secure my account" }
        ]
      },
      {
        "id": "alert-uuid-2",
        "type": "NEW_DEVICE_LOGIN",
        "severity": "MEDIUM",
        "title": "New device added",
        "description": "Chrome on Windows was added to your account",
        "deviceName": "Chrome on Windows",
        "location": "Dar es Salaam, Tanzania",
        "status": "READ",
        "actionTaken": null,
        "createdAt": "2025-01-11T14:30:00",
        "actions": [
          { "action": "DISMISS", "label": "This was me" },
          { "action": "REMOVE_DEVICE", "label": "Remove this device" }
        ]
      }
    ],
    "unreadCount": 1,
    "totalAlerts": 2
  }
}

Dismiss Security Alert

POST /api/v1/auth/security-alerts/{alertId}/dismiss

Request:

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

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Alert dismissed",
  "action_time": "2025-01-11T18:30:00",
  "data": {
    "alertId": "alert-uuid-1",
    "status": "DISMISSED",
    "unreadCount": 0
  }
}

New Database Entities for Device Trust

UserDevice

- id (UUID)
- userId (UUID) FK → AccountEntity
- deviceId (String) - fingerprint from client (unique per user)
- deviceName (String) - "iPhone 15 Pro", "Chrome on Windows"
- deviceType (Enum) - MOBILE_IOS, MOBILE_ANDROID, WEB_BROWSER, DESKTOP_APP
- userAgent (String)
- lastIpAddress (String)
- lastLocation (String)
- isTrusted (Boolean)
- trustExpiresAt (LocalDateTime) - 30 days sliding window
- lastActiveAt (LocalDateTime)
- createdAt (LocalDateTime)
- updatedAt (LocalDateTime)

Indexes:
- (userId, deviceId) UNIQUE
- (userId, isTrusted)
- (lastActiveAt)

LoginEvent

- id (UUID)
- userId (UUID) FK → AccountEntity
- deviceId (UUID) FK → UserDevice (nullable for failed attempts)
- loginMethod (Enum) - PASSWORD, OTP_SMS, OTP_EMAIL, GOOGLE, APPLE
- ipAddress (String)
- location (String)
- status (Enum) - SUCCESS, FAILED_PASSWORD, FAILED_OTP, BLOCKED, REQUIRES_OTP
- requiresOtp (Boolean)
- otpReason (Enum) - NEW_DEVICE, INACTIVE_DEVICE, SUSPICIOUS, MANUAL
- createdAt (LocalDateTime)

Indexes:
- (userId, createdAt)
- (userId, status)
- (ipAddress, createdAt) - for rate limiting

SecurityAlert

- id (UUID)
- userId (UUID) FK → AccountEntity
- loginEventId (UUID) FK → LoginEvent (nullable)
- type (Enum) - SUSPICIOUS_LOGIN, NEW_DEVICE_LOGIN, FAILED_ATTEMPTS, PASSWORD_CHANGED, etc.
- severity (Enum) - LOW, MEDIUM, HIGH, CRITICAL
- title (String)
- description (String)
- deviceName (String)
- ipAddress (String)
- location (String)
- status (Enum) - UNREAD, READ, DISMISSED, ACTIONED
- actionTaken (String)
- createdAt (LocalDateTime)
- readAt (LocalDateTime)
- dismissedAt (LocalDateTime)

Indexes:
- (userId, status)
- (userId, createdAt)

Device Trust Configuration

# application.yml
device:
  trust:
    enabled: true
    window-days: 30                    # Trust window duration
    max-devices-per-user: 10           # Max trusted devices
    auto-cleanup-days: 90              # Remove inactive devices after
    
login:
  security:
    max-failed-attempts: 5             # Before temporary lock
    lock-duration-minutes: 30          # Temporary lock duration
    suspicious-countries: [XX, YY]     # Countries requiring extra verification
    alert-on-new-country: true         # Send alert on new country login

New NextGate Auth & Onboarding Flow UI (DEPRECATED)

1. Sign Up Flow

Path A: Phone User

Enter phone number
       │
       ▼
   Verify OTP
       │
       ▼
  Enter age & name
       │
       ▼
  Pick username
       │
       ▼
 Pick interests (min 3)
       │
       ▼
 Profile setup (optional)
       │
       ▼
   Home Feed 🎉
(device registered)

── Post-onboarding ──
Soft nudge to add email
(banner only, never blocking)

Path B: Email User

Enter email address
       │
       ▼
   Verify OTP
       │
       ▼
  Enter age & name
       │
       ▼
  Pick username
       │
       ▼
 Verify phone number ← REQUIRED (Identity)
       │
       ▼
 Pick interests (min 3)
       │
       ▼
 Profile setup (optional)
       │
       ▼
   Home Feed 🎉
(device registered)

Path C: Google / Apple OAuth

Tap Google / Apple
       │
       ▼
   OAuth authorize
       │
       ▼
  Enter age & name
  (pre-filled from OAuth)
       │
       ▼
  Pick username
  (suggestions from name)
       │
       ▼
 Verify phone number ← REQUIRED (Identity)
       │
       ▼
 Pick interests (min 3)
       │
       ▼
 Profile setup (optional)
  (profile pic pre-filled)
       │
       ▼
   Home Feed 🎉
(device registered)

2. Onboarding Steps Detail

Step Summary

# Step Applies To Endpoint
0 OTP / OAuth entry All /auth/start + /auth/verify or /auth/login/oauth
1 Age & name All /auth/onboarding/age
2 Username All /auth/onboarding/username
3 Contact verify (PHONE) Email + OAuth only /auth/onboarding/contact/initiate + /contact/verify
4 Interests All /auth/onboarding/interests
5 Profile All /auth/onboarding/profile

Contact Verification Rule

Signup Method Required Contact Reason
Phone Email (optional, post-onboarding) Phone = real identity, M-Pesa already linked
Email Phone (blocking) Needed for M-Pesa payments
Google / Apple Phone (blocking) OAuth gives email, phone still required for payments

Onboarding Step Sequence

Phone user:

INITIATED → OTP_VERIFIED → AGE_VERIFIED → USERNAME_SET → INTERESTS_SELECTED → PROFILE_COMPLETED → COMPLETED

Email / OAuth user:

INITIATED → OTP_VERIFIED → AGE_VERIFIED → USERNAME_SET → CONTACT_VERIFIED → INTERESTS_SELECTED → PROFILE_COMPLETED → COMPLETED

Token Expiry

Token Expiry Purpose
onboardingToken 7 days Active onboarding flow, resets each step
onboardingRefreshToken 30 days Safety net if token expires mid-flow
accessToken Configured Normal app access
refreshToken Configured Access token rotation

3. Contact Verification — Inline UX (3 states, 1 page)

STATE A — Input
┌──────────────────────────────────────┐
│  📱 Add your phone number            │
│                                      │
│  Your email j••••@g••••.com ✅       │
│                                      │
│  We need your phone for:             │
│  • Offline notifications             │
│  • Account security & recovery       │
│                                      │
│  Phone: [+255______________]         │
│  [Send Code →]                       │
└──────────────────────────────────────┘

STATE B — OTP Input
┌──────────────────────────────────────┐
│  Enter code sent to ••• ••• ••50     │
│                                      │
│  [_] [_] [_] [_] [_] [_]            │
│                                      │
│  [Edit number]  Resend (60s)         │
└──────────────────────────────────────┘

STATE C — Edit (tapping [Edit number] transforms inline)
┌──────────────────────────────────────┐
│  New number: [+255______________]    │
│  [Send new code →]    [Cancel]       │
└──────────────────────────────────────┘

Previous OTP invalidated immediately on edit. No page navigation — single page, three inline states.


4. Login Flow

Identifier Detection

Input pattern Detected as
Starts with + Phone
Contains @ + domain Email
Otherwise Username

Complete Login Flow

┌─────────────────────────────────────┐
│           Login Screen              │
└─────────────────┬───────────────────┘
                  │
      ┌───────────┼───────────┐
      │           │           │
      ▼           ▼           ▼
  Google/     Phone/Email   Username
  Apple           │           │
      │           │           │
      ▼           ▼           ▼
   OAuth      Detect      Find account
      │       account          │
      │           │            │
      │           ▼            ▼
      │      Has password?  Show masked
      │           │         OTP destination
      │     ┌─────┴─────┐        │
      │     ▼           ▼        │
      │    No          Yes       │
      │     │           │        │
      │     │     ┌─────┴─────┐  │
      │     │     ▼           ▼  │
      │     │   [OTP]    [Password]
      │     │     │           │  │
      │     ▼     ▼           ▼  │
      │    OTP Screen    Password │
      │         │        Screen  │
      │         │           │    │
      │         └─────┬─────┘    │
      │               │          │
      ▼               ▼          ▼
 Device Check    Device Check   OTP Screen
      │               │              │
      ▼               ▼              ▼
   (see §5)        (see §5)      Device Check
                                     │
                                     ▼
                                  (see §5)

5. Device Trust Check

After Authentication

User Authenticated
       │
       ▼
┌──────────────────┐
│  Check Device    │
│  • Device ID     │
│  • Fingerprint   │
│  • IP / Location │
└────────┬─────────┘
         │
         ▼
   Is Device Known?
         │
    ┌────┴────┐
    ▼         ▼
   Yes        No
    │         │
    ▼         ▼
  Home 🎉  Login Method?
              │
      ┌───────┼───────┐
      ▼       ▼       ▼
    OTP   Password  OAuth
      │       │       │
      ▼       ▼       ▼
   Home 🎉  OTP     OTP
          Required  Required
              │       │
              ▼       ▼
           Verify   Verify
              │       │
              ▼       ▼
           Register Device
              │
              ▼
           Home 🎉

Device Trust Rules

Login Method New Device Action Reason
OTP None OTP = already verified
Password Require OTP Password could be stolen
OAuth Require OTP Token could be compromised

6. Risk Scoring

Signal Low Risk High Risk
Location Same country New country
Device Similar OS Different OS
IP Normal VPN / Proxy
Account Active Dormant
Time Normal hours Unusual hours

Action by Risk

Risk Score
    │
    ├── Low Risk  ──► Soft verify (email link, approve from known device)
    │
    └── High Risk ──► Phone OTP required

7. UI Screens

Sign Up Screen

┌─────────────────────────────────────┐
│                                     │
│         Create Account 🚀           │
│                                     │
│  ┌───────────────────────────────┐  │
│  │ Phone or Email                │  │
│  └───────────────────────────────┘  │
│                                     │
│  ┌───────────────────────────────┐  │
│  │          Continue             │  │
│  └───────────────────────────────┘  │
│                                     │
│           ── or ──                  │
│                                     │
│      [Google]      [Apple]          │
│                                     │
│   Already have account? Login       │
│                                     │
└─────────────────────────────────────┘

OTP Verification Screen

┌─────────────────────────────────────┐
│                                     │
│          Verify OTP 🔐              │
│                                     │
│   Enter code sent to:               │
│   +255 712 345 678                  │
│                                     │
│   ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐
│   │   │ │   │ │   │ │   │ │   │ │   │
│   └───┘ └───┘ └───┘ └───┘ └───┘ └───┘
│                                     │
│   Resend code (0:45)                │
│                                     │
└─────────────────────────────────────┘

Age & Name Screen

┌─────────────────────────────────────┐
│                                     │
│        Let's get started 📅         │
│                                     │
│   First name:                       │
│   ┌───────────────────────────────┐ │
│   │ John                          │ │
│   └───────────────────────────────┘ │
│                                     │
│   Last name:                        │
│   ┌───────────────────────────────┐ │
│   │ Doe                           │ │
│   └───────────────────────────────┘ │
│                                     │
│   Date of birth:                    │
│   ┌───────────────────────────────┐ │
│   │ 1999 / 05 / 15                │ │
│   └───────────────────────────────┘ │
│                                     │
│  ┌───────────────────────────────┐  │
│  │          Continue             │  │
│  └───────────────────────────────┘  │
│                                     │
└─────────────────────────────────────┘

Username Screen

┌─────────────────────────────────────┐
│                                     │
│        Pick Username 🏷️            │
│                                     │
│   ┌───────────────────────────────┐ │
│   │ @johndoe99                    │ │
│   └───────────────────────────────┘ │
│                                     │
│   ✅ @johndoe99 is available        │
│                                     │
│   Suggestions:                      │
│   @johndoe_tz  @jdoe99  @the_john  │
│                                     │
│  ┌───────────────────────────────┐  │
│  │          Continue             │  │
│  └───────────────────────────────┘  │
│                                     │
└─────────────────────────────────────┘

Contact Verification Screen (Email/OAuth users only)

┌─────────────────────────────────────┐
│                                     │
│     Add your phone number 📱        │
│                                     │
│   Your email j•••@g••••.com ✅      │
│                                     │
│   Needed for:                       │
│   • SMS reminders                   │
│   • Account security and recovery   │
│                                     │
│   ┌───────────────────────────────┐ │
│   │ +255 _____________________    │ │
│   └───────────────────────────────┘ │
│                                     │
│  ┌───────────────────────────────┐  │
│  │          Send Code            │  │
│  └───────────────────────────────┘  │
│                                     │
└─────────────────────────────────────┘

Interests Screen

┌─────────────────────────────────────┐
│                                     │
│     Pick Your Interests 🎯          │
│     (at least 3)                    │
│                                     │
│   ┌─────────┐ ┌─────────┐ ┌───────┐ │
│   │ Fashion │ │  Tech   │ │ Music │ │
│   └─────────┘ └─────────┘ └───────┘ │
│   ┌─────────┐ ┌─────────┐ ┌───────┐ │
│   │ Sports  │ │  Food   │ │Gaming │ │
│   └─────────┘ └─────────┘ └───────┘ │
│   ┌─────────┐ ┌─────────┐ ┌───────┐ │
│   │ Travel  │ │   Art   │ │Fitness│ │
│   └─────────┘ └─────────┘ └───────┘ │
│                                     │
│  ┌───────────────────────────────┐  │
│  │          Continue             │  │
│  └───────────────────────────────┘  │
│                                     │
└─────────────────────────────────────┘

Profile Setup Screen

┌─────────────────────────────────────┐
│                                     │
│      Complete Profile ✨            │
│      (optional)                     │
│                                     │
│          ┌─────────┐                │
│          │   📷    │                │
│          └─────────┘                │
│          Add photo                  │
│                                     │
│   Display name:                     │
│   ┌───────────────────────────────┐ │
│   │ John Doe                      │ │
│   └───────────────────────────────┘ │
│                                     │
│   Bio:                              │
│   ┌───────────────────────────────┐ │
│   │ Music lover | Dar es Salaam 🇹🇿│ │
│   └───────────────────────────────┘ │
│                                     │
│  ┌──────────────┐ ┌──────────────┐  │
│  │   Complete   │ │ Skip for now │  │
│  └──────────────┘ └──────────────┘  │
│                                     │
└─────────────────────────────────────┘

Login Screen

┌─────────────────────────────────────┐
│                                     │
│         Welcome Back 👋             │
│                                     │
│  ┌───────────────────────────────┐  │
│  │ Phone, email, or username     │  │
│  └───────────────────────────────┘  │
│                                     │
│  ┌───────────────────────────────┐  │
│  │          Continue             │  │
│  └───────────────────────────────┘  │
│                                     │
│           ── or ──                  │
│                                     │
│      [Google]      [Apple]          │
│                                     │
│   Don't have account? Sign up       │
│                                     │
└─────────────────────────────────────┘

Login Method Choice (if password exists)

┌─────────────────────────────────────┐
│                                     │
│      How do you want to login?      │
│                                     │
│  ┌───────────────────────────────┐  │
│  │      📱 Send me OTP           │  │
│  └───────────────────────────────┘  │
│                                     │
│  ┌───────────────────────────────┐  │
│  │      🔑 Use password          │  │
│  └───────────────────────────────┘  │
│                                     │
└─────────────────────────────────────┘

OTP Destination Choice (username login)

┌─────────────────────────────────────┐
│                                     │
│      Send OTP to:                   │
│                                     │
│      ○ ••• ••• ••45                 │
│      ○ j••••••@g••••.com            │
│                                     │
│  ┌───────────────────────────────┐  │
│  │          Send OTP             │  │
│  └───────────────────────────────┘  │
│                                     │
└─────────────────────────────────────┘

New Device Detected

┌─────────────────────────────────────┐
│                                     │
│      🆕 New device detected         │
│                                     │
│   For your security, verify it's    │
│   you with a one-time code.         │
│                                     │
│      ○ ••• ••• ••45                 │
│      ○ j••••••@g••••.com            │
│                                     │
│  ┌───────────────────────────────┐  │
│  │          Send OTP             │  │
│  └───────────────────────────────┘  │
│                                     │
└─────────────────────────────────────┘

8. Security Settings (Post-onboarding)

┌─────────────────────────────────────┐
│                                     │
│      Security Settings 🔒           │
│                                     │
├─────────────────────────────────────┤
│                                     │
│   Phone                             │
│   ┌───────────────────────────────┐ │
│   │ +255 712 XXX XXX   ✅ Verified│ │
│   └───────────────────────────────┘ │
│                                     │
│   Email                             │
│   ┌───────────────────────────────┐ │
│   │ Not added          [Add now]  │ │  ← phone users see this
│   └───────────────────────────────┘ │
│   💡 Add email for ticket delivery  │
│                                     │
│   Password                          │
│   ┌───────────────────────────────┐ │
│   │ Not set               [Add]   │ │
│   └───────────────────────────────┘ │
│   💡 Optional extra security        │
│                                     │
├─────────────────────────────────────┤
│                                     │
│   Linked Accounts                   │
│   ┌───────────────────────────────┐ │
│   │ Google    Not linked  [Link]  │ │
│   │ Apple     Not linked  [Link]  │ │
│   └───────────────────────────────┘ │
│                                     │
└─────────────────────────────────────┘

9. Summary

Sign Up

Path Steps
Phone OTP → Age → Username → Interests → Profile → Home
Email OTP → Age → Username → Verify Phone → Interests → Profile → Home
OAuth Authorize → Age → Username → Verify Phone → Interests → Profile → Home

Login

Has Password? Options
No OTP only
Yes Choose: OTP or Password

Device Verification

Login Method New Device Action
OTP None (OTP = verification)
Password OTP required
OAuth OTP required

Post-Onboarding Nudges (Phone users)

Feature Requires Email? Behaviour
Browse feed No Free
Buy ticket No Free
Receive ticket via email Yes Nudge to add email
Account recovery Yes Nudge to add email

NextGate Authentication Specification v1.0 (DEPRECATED)

1. Overview

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

Principle Implementation
Passwordless default OTP-based, password optional (add later in settings)
Device trust Hardware-bound keys (mobile), fingerprint (web)
Risk-based Dynamic verification based on risk score
Age-gated 18+ full access, tiered restrictions below

2. Sign Up Flow

┌─────────────────────────────────────┐
│      Enter phone/email              │
│      OR tap Google/Apple            │
└─────────────────┬───────────────────┘
                  │
        ┌─────────┴─────────┐
        ▼                   ▼
   Phone/Email            OAuth
        │                   │
        ▼                   │
    Verify OTP              │
        │                   │
        └─────────┬─────────┘
                  ▼
┌─────────────────────────────────────┐
│      Enter Birthdate (Age Gate)     │
└─────────────────┬───────────────────┘
                  │
      ┌───────────┼───────────┐
      ▼           ▼           ▼
    18+        13-17      Under 13
      │           │           │
      ▼           ▼           ▼
   Continue   Restricted    Block
      │        + Parent      │
      │        Consent       ▼
      │           │        "Come back
      └─────┬─────┘         later"
            ▼
┌─────────────────────────────────────┐
│      Pick Username (@handle)        │
└─────────────────┬───────────────────┘
                  ▼
┌─────────────────────────────────────┐
│      Select Interests (min 3)       │
└─────────────────┬───────────────────┘
                  ▼
┌─────────────────────────────────────┐
│      Profile Setup (optional skip)  │
│      • Display name                 │
│      • Photo                        │
│      • Bio                          │
└─────────────────┬───────────────────┘
                  ▼
              Home 🎉
        (Device registered)

3. Login Flow

┌─────────────────────────────────────┐
│  Enter phone/email/username         │
│  OR tap Google/Apple                │
└─────────────────┬───────────────────┘
                  │
        ┌─────────┴─────────┐
        ▼                   ▼
   Identifier             OAuth
        │                   │
        ▼                   │
   Detect type:             │
   • + prefix → Phone       │
   • @domain → Email        │
   • else → Username        │
        │                   │
        ▼                   │
   Find account             │
        │                   │
   ┌────┴────┐              │
   ▼         ▼              │
Has PWD?  No PWD            │
   │         │              │
   ▼         │              │
┌────────┐   │              │
│Choose: │   │              │
│• OTP   │   │              │
│• PWD   │   │              │
└───┬────┘   │              │
    │        │              │
    └────┬───┘              │
         ▼                  │
   ┌─────────────┐          │
   │ If username │          │
   │ login: show │          │
   │ masked OTP  │          │
   │ destination │          │
   └──────┬──────┘          │
          │                 │
          ▼                 │
   Verify OTP/PWD           │
          │                 │
          └────────┬────────┘
                   ▼
            Device Check
                   │
          (see section 5)

OTP Destination (Username Login)

┌─────────────────────────────────────┐
│      Send OTP to:                   │
│                                     │
│      ○ ••• ••• ••45                 │
│      ○ j••••••@g••••.com            │
│                                     │
│      [Send OTP]                     │
└─────────────────────────────────────┘

If only one exists → skip choice, send directly

4. Login Method Summary

Has Password? Login Options
No OTP only (passwordless)
Yes Choose: OTP or Password
Login Method New Device Handling
OTP None needed — OTP is verification
Password OTP required on new device
OAuth OTP required on new device

5. Device Trust

Platform Strategy

Platform Method Trust Level
iOS Secure Enclave key pair ⭐⭐⭐⭐⭐ High
Android StrongBox/TEE Keystore ⭐⭐⭐⭐⭐ High
Web Fingerprint + session key ⭐⭐⭐ Medium

Mobile Device Registration

First App Launch
       │
       ▼
┌─────────────────────────────────────┐
│ Generate key pair in Secure Enclave │
│ • Private key: NEVER leaves device  │
│ • Public key: sent to server        │
└─────────────────┬───────────────────┘
                  ▼
        Device registered ✅

Login with Device Verification

┌─────────────────────────────────────┐
│ 1. Client: GET /auth/challenge      │
│    Server returns: { nonce: "xyz" } │
└─────────────────┬───────────────────┘
                  ▼
┌─────────────────────────────────────┐
│ 2. Client: Sign nonce with          │
│    hardware private key             │
└─────────────────┬───────────────────┘
                  ▼
┌─────────────────────────────────────┐
│ 3. Client: POST /auth/login         │
│    { signature, deviceId, nonce }   │
└─────────────────┬───────────────────┘
                  ▼
┌─────────────────────────────────────┐
│ 4. Server: Verify signature with    │
│    stored public key                │
│    • Valid → trusted device ✅      │
│    • Invalid → block ❌             │
└─────────────────────────────────────┘

Why Attacker Fails

Attacker has:     ✅ Password, ✅ DeviceId, ✅ Nonce
Attacker needs:   ❌ Private key (locked in victim's hardware)
Result:           ❌ Cannot forge signature → Attack fails

6. Risk Scoring

Signals & Weights

Signal Low (0) Medium High
Location Same city Same country (+10) New country (+25)
Device Known (0) Similar OS (+10) New OS (+20)
IP Normal ISP (0) Different ISP (+10) VPN/TOR (+30)
Time Normal hours (0) Unusual (+10) 2-5 AM (+15)
Failed attempts None (0) 1-2 (+10) 3+ (+25)
Velocity Normal (0) Multiple (+15) Rapid (+30)
Device signature Valid (-20) Missing (+15) Invalid (+40)

Thresholds & Actions

Score Risk Level Action
0-30 🟢 Low Allow
31-60 🟡 Medium Soft verify (email link)
61-85 🔴 High Phone OTP required
86-100 ⛔ Critical Block + alert user

Impossible Travel

Last login: Dar es Salaam at 10:00 AM
This login: London at 10:30 AM
Distance: 7,500 km in 30 min = impossible

→ +40 points → likely compromised

7. Age Restriction

Tiers

Age Access Level
18+ Full access
13-17 Restricted (no purchases, filtered content)
Under 13 Blocked (COPPA)

Blocked User Handling

User blocked (underage)
       │
       ▼
Tries again with same phone/email
       │
       ▼
┌─────────────────────────────────────┐
│ System checks:                      │
│ • Phone/email in blocked list?      │
│ • Device fingerprint matches?       │
│ • Same IP?                          │
└─────────────────┬───────────────────┘
                  ▼
            Block signup
       "Cannot register at this time"

8. Username Rules

Change Limits (Anti-Fraud)

Account Age Allowed Changes
Day 0 (today) 5 changes
1-30 days 1 per month
1-12 months 1 per month
12+ months 1 per year
SYSTEM accounts Never

Account Types

Type Examples Username Change
NORMAL Regular users Limited (above)
SYSTEM @nextgate, @admin, @support Never
VERIFIED @nike, @cocacola Requires approval

9. Session Management

Sign Out Options

Action What It Does Requires
Sign out Current device only Nothing
Sign out others All except current OTP/Password
Sign out all Everything OTP/Password

Active Sessions View

┌─────────────────────────────────────┐
│ 📱 iPhone 14 Pro                    │
│    Dar es Salaam • Active now       │
│    This device                 [●]  │
├─────────────────────────────────────┤
│ 💻 Chrome on Windows                │
│    Nairobi • 2 hours ago            │
│                          [Sign out] │
├─────────────────────────────────────┤
│ [Sign out other devices]            │
│ [Sign out all devices] ⚠️           │
└─────────────────────────────────────┘

10. Security Settings

┌─────────────────────────────────────┐
│ Security Settings 🔒                │
├─────────────────────────────────────┤
│ Phone: +255 712 •••456   ✅ Verified │
│ Email: j••••@email.com   ✅ Verified │
├─────────────────────────────────────┤
│ Password: Not set           [Add]   │
│ 💡 Optional extra security          │
├─────────────────────────────────────┤
│ Linked Accounts:                    │
│ Google: Not linked         [Link]   │
│ Apple: Not linked          [Link]   │
└─────────────────────────────────────┘

11. API Endpoints

Auth - Signup

Endpoint Purpose
POST /auth/signup/initiate Start signup (phone/email)
POST /auth/signup/verify-otp Verify OTP
POST /auth/signup/age Submit birthdate
POST /auth/signup/username Set username
POST /auth/signup/interests Select interests
POST /auth/signup/profile Complete profile (optional)

Auth - Login

Endpoint Purpose
POST /auth/login/initiate Start login
POST /auth/login/otp Login with OTP
POST /auth/login/password Login with password
GET /auth/challenge Get nonce for device signing

Auth - Device

Endpoint Purpose
POST /auth/device/register Register device (public key)
POST /auth/device/verify Verify new device OTP
GET /auth/devices List trusted devices
DELETE /auth/devices/{id} Revoke device

Auth - Session

Endpoint Purpose
GET /auth/sessions List active sessions
POST /auth/sign-out Current device
POST /auth/sign-out-others All except current
POST /auth/sign-out-all Everything

12. Database Entities

New Entities

Entity Purpose
DeviceKey Hardware-bound public keys
UserSession Active sessions
LoginAttempt Risk scoring data
BlockedUser Blocked identifiers/devices
InterestCategory Admin-managed interests
UserInterest User selections
UsernameChangeHistory Track changes

AccountEntity Changes

Field Change
password Make nullable
birthDate Add
displayName Add
accountType Add (NORMAL, SYSTEM, VERIFIED)
accountTier Add (FULL, RESTRICTED, MINOR)
authProvider Add (PHONE, EMAIL, GOOGLE, APPLE)
onboardingStep Add
usernameLastChangedAt Add
usernameChangeCount Add

13. Enums

AuthProvider: PHONE, EMAIL, GOOGLE, APPLE
AccountType: NORMAL, SYSTEM, VERIFIED
AccountTier: FULL, RESTRICTED, MINOR
DevicePlatform: IOS, ANDROID, WEB
TrustLevel: HIGH, MEDIUM, LOW
RiskLevel: LOW, MEDIUM, HIGH, CRITICAL

14. Quick Reference

Onboarding Steps

1. Signup (phone/email/OAuth)
2. Verify OTP (if phone/email)
3. Birthdate (age gate)
4. Username
5. Interests (min 3)
6. Profile (optional)

Device Verification Matrix

Login Method Known Device New Device
OTP → Home → Home (OTP is verification)
Password → Home → OTP required → Home
OAuth → Home → OTP required → Home

Risk Score Quick Reference

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

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

When Does What Happen?

Action When Where
Device Registration First app launch (before any auth) Mobile only
Web Session Init First visit (before any auth) Web only
Risk Scoring After credentials verified, before home Login only
Device Verification OTP After risk score (if needed) Login only (new device + password/OAuth)

Master Flow Chart

┌─────────────────────────────────────────────────────────────────────────────┐
│                           APP/WEB FIRST LAUNCH                              │
└─────────────────────────────────┬───────────────────────────────────────────┘
                                  │
                    ┌─────────────┴─────────────┐
                    ▼                           ▼
              Mobile App                    Web Browser
                    │                           │
                    ▼                           ▼
        ┌───────────────────────┐   ┌───────────────────────┐
        │ Generate key pair in  │   │ Generate session ID   │
        │ Secure Enclave/TEE    │   │ + collect fingerprint │
        │                       │   │                       │
        │ Store private key     │   │ Store in memory       │
        │ (hardware, never      │   │ (ephemeral)           │
        │ leaves device)        │   │                       │
        └───────────┬───────────┘   └───────────┬───────────┘
                    │                           │
                    └─────────────┬─────────────┘
                                  │
                                  ▼
                    ┌─────────────────────────┐
                    │   Show Login/Signup     │
                    │   Screen                │
                    └─────────────┬───────────┘
                                  │
                    ┌─────────────┴─────────────┐
                    ▼                           ▼
                 SIGNUP                       LOGIN
                    │                           │
                    ▼                           ▼
┌───────────────────────────────┐ ┌───────────────────────────────────────────┐
│         SIGNUP FLOW           │ │              LOGIN FLOW                   │
├───────────────────────────────┤ ├───────────────────────────────────────────┤
│                               │ │                                           │
│ 1. Enter phone/email/OAuth    │ │ 1. Enter identifier (phone/email/username)│
│            │                  │ │    OR tap OAuth                           │
│            ▼                  │ │            │                              │
│ 2. Verify OTP (if not OAuth)  │ │            ▼                              │
│            │                  │ │ 2. GET /auth/challenge ◄── GET NONCE      │
│            ▼                  │ │    Server returns { nonce: "xyz" }        │
│ 3. Enter birthdate (age gate) │ │            │                              │
│            │                  │ │            ▼                              │
│    ┌───────┴───────┐          │ │ 3. Sign nonce with hardware key (mobile)  │
│    ▼               ▼          │ │    OR attach fingerprint (web)            │
│  18+            <18           │ │            │                              │
│    │          (block/         │ │            ▼                              │
│    │          restrict)       │ │ 4. Find account                           │
│    ▼                          │ │            │                              │
│ 4. Pick username              │ │    ┌───────┴───────┐                      │
│            │                  │ │    ▼               ▼                      │
│            ▼                  │ │ Has password?   No password               │
│ 5. Select interests (min 3)   │ │    │               │                      │
│            │                  │ │    ▼               │                      │
│            ▼                  │ │ Show choice:      │                      │
│ 6. Profile setup (optional)   │ │ • OTP             │                      │
│            │                  │ │ • Password        │                      │
│            ▼                  │ │    │               │                      │
│ 7. Register device with       │ │    └───────┬───────┘                      │
│    server (send public key)   │ │            ▼                              │
│            │                  │ │ 5. POST /auth/login with:                 │
│            ▼                  │ │    • credentials (OTP or password)        │
│        HOME 🎉                │ │    • deviceId                             │
│   (Device trusted)            │ │    • nonce                                │
│                               │ │    • signature ◄── SIGNED NONCE           │
└───────────────────────────────┘ │            │                              │
                                  │            ▼                              │
                                  │ 6. Server validates BOTH:                 │
                                  │    • Credentials ✓                        │
                                  │    • Signature ✓ (if known device)        │
                                  │            │                              │
                                  │            ▼                              │
                                  │ ┌─────────────────────────────────────┐   │
                                  │ │      DEVICE CHECK (After Auth)      │   │
                                  │ ├─────────────────────────────────────┤   │
                                  │ │                                     │   │
                                  │ │  Is device known?                   │   │
                                  │ │         │                           │   │
                                  │ │    ┌────┴────┐                      │   │
                                  │ │    ▼         ▼                      │   │
                                  │ │  Known    Unknown                   │   │
                                  │ │    │         │                      │   │
                                  │ │    │    Login method?               │   │
                                  │ │    │         │                      │   │
                                  │ │    │    ┌────┴────────┐             │   │
                                  │ │    │    ▼            ▼              │   │
                                  │ │    │   OTP      PWD/OAuth           │   │
                                  │ │    │    │            │              │   │
                                  │ │    │    │     Calculate risk        │   │
                                  │ │    │    │            │              │   │
                                  │ │    │    │    ┌───────┴───────┐      │   │
                                  │ │    │    │    ▼               ▼      │   │
                                  │ │    │    │  Low/Med         High     │   │
                                  │ │    │    │  (0-60)         (61+)     │   │
                                  │ │    │    │    │               │      │   │
                                  │ │    │    │    ▼               ▼      │   │
                                  │ │    │    │  Soft verify   Phone OTP  │   │
                                  │ │    │    │  (email link)   required  │   │
                                  │ │    │    │    │               │      │   │
                                  │ │    │    │    └───────┬───────┘      │   │
                                  │ │    │    │            │              │   │
                                  │ │    │    │    Register new device    │   │
                                  │ │    │    │    (send public key)      │   │
                                  │ │    │    │            │              │   │
                                  │ │    └────┴────────────┘              │   │
                                  │ │                                     │   │
                                  │ └─────────────────┬───────────────────┘   │
                                  │                   │                       │
                                  │                   ▼                       │
                                  │               HOME 🎉                     │
                                  │                                           │
                                  └───────────────────────────────────────────┘

Challenge-Response: Detailed Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                        CHALLENGE-RESPONSE FLOW                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   STEP 1: User enters identifier (before submitting credentials)            │
│                                                                             │
│   ┌──────────┐         GET /auth/challenge            ┌──────────┐         │
│   │  Client  │ ─────────────────────────────────────▶ │  Server  │         │
│   └──────────┘                                        └────┬─────┘         │
│                                                            │               │
│                                                            ▼               │
│                                                   Generate nonce           │
│                                                   (random, expires 60s)    │
│                                                            │               │
│   ┌──────────┐         { nonce: "xyz123" }           ┌────┴─────┐         │
│   │  Client  │ ◀───────────────────────────────────── │  Server  │         │
│   └────┬─────┘                                        └──────────┘         │
│        │                                                                    │
│        ▼                                                                    │
│   STEP 2: Client signs nonce (mobile only)                                  │
│                                                                             │
│   ┌─────────────────────────────────────────┐                              │
│   │  signature = sign(                      │                              │
│   │    nonce + timestamp,                   │                              │
│   │    privateKey  ◄── from Secure Enclave  │                              │
│   │  )                                      │                              │
│   └─────────────────────────────────────────┘                              │
│                                                                             │
│   STEP 3: Submit login with signature                                       │
│                                                                             │
│   ┌──────────┐      POST /auth/login              ┌──────────┐             │
│   │  Client  │ ─────────────────────────────────▶ │  Server  │             │
│   └──────────┘      {                             └────┬─────┘             │
│                       identifier: "user@mail.com",     │                   │
│                       otp: "123456",                   │                   │
│                       deviceId: "dev_abc",             │                   │
│                       nonce: "xyz123",                 │                   │
│                       signature: "abc123..."           │                   │
│                     }                                  │                   │
│                                                        ▼                   │
│   STEP 4: Server validates                    ┌───────────────────┐        │
│                                               │ 1. Nonce valid?   │        │
│                                               │    (not expired,  │        │
│                                               │    not reused)    │        │
│                                               │         │         │        │
│                                               │         ▼         │        │
│                                               │ 2. Credentials?   │        │
│                                               │    (OTP/password) │        │
│                                               │         │         │        │
│                                               │         ▼         │        │
│                                               │ 3. Signature?     │        │
│                                               │    (verify with   │        │
│                                               │    stored pubkey) │        │
│                                               └─────────┬─────────┘        │
│                                                         │                  │
│                                               ┌─────────┴─────────┐        │
│                                               ▼                   ▼        │
│                                           All pass            Any fail     │
│                                               │                   │        │
│                                               ▼                   ▼        │
│                                           Continue            Reject       │
│                                           to device           login        │
│                                           check                            │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

When Challenge Happens: Summary

Scenario Challenge? Signature Validated?
Signup ❌ No ❌ No (device not registered yet)
Login - Known device (mobile) ✅ Yes ✅ Yes (must match)
Login - Known device (web) ✅ Yes 🟡 Fingerprint checked
Login - Unknown device ✅ Yes ❌ No pubkey stored yet

Request/Response Example

1. Get Challenge:

GET /auth/challenge

Response:
{
  "nonce": "ch_7f8a9b2c3d4e5f6g",
  "expiresIn": 60
}

2. Login with Signature:

POST /auth/login

{
  "identifier": "alex@email.com",
  "otp": "123456",
  "deviceId": "dev_iphone14_abc123",
  "nonce": "ch_7f8a9b2c3d4e5f6g",
  "signature": "MEUCIQD2k3n...(base64 signed data)...",
  "timestamp": "2026-01-12T10:30:00Z"
}

Signup vs Login: What Happens Where

Step Signup Login
Device key generation ✅ Before auth (app launch) ✅ Before auth (app launch)
OTP verification ✅ To verify phone/email ✅ As login method OR device verify
Age gate ✅ After OTP ❌ Not needed
Username ✅ Required ❌ Not needed
Interests ✅ Required ❌ Not needed
Risk scoring ❌ Not needed (new account) ✅ After credentials verified
Device verification OTP ❌ Not needed (first device) ✅ If new device + high risk
Device registration ✅ End of onboarding ✅ After device verification

Risk Scoring: When & How

                    Login credentials verified
                              │
                              ▼
                    ┌─────────────────────┐
                    │  Collect signals:   │
                    │  • IP / Location    │
                    │  • Device info      │
                    │  • User agent       │
                    │  • Timestamp        │
                    │  • Login history    │
                    └──────────┬──────────┘
                               │
                               ▼
                    ┌─────────────────────┐
                    │  Calculate score:   │
                    │  Location:    +25   │
                    │  Device:      +20   │
                    │  IP:          +0    │
                    │  Time:        +10   │
                    │  Velocity:    +0    │
                    │  ──────────────     │
                    │  Total:       55    │
                    └──────────┬──────────┘
                               │
                               ▼
                    ┌─────────────────────┐
                    │  Score: 55 = MEDIUM │
                    │  Action: Soft verify│
                    └─────────────────────┘

16. Logout / Sign Out

Options

Action What It Does Requires
Sign out End current session Nothing
Sign out other devices End all except current OTP or Password
Sign out all devices End everything OTP or Password

Flow

User taps "Sign out"
        │
        ├── "Sign out" (this device)
        │         │
        │         ▼
        │   Revoke current token
        │         │
        │         ▼
        │   → Login screen
        │
        ├── "Sign out other devices"
        │         │
        │         ▼
        │   Verify (OTP or Password)
        │         │
        │         ▼
        │   Revoke all tokens except current
        │         │
        │         ▼
        │   "Other devices signed out" ✅
        │
        └── "Sign out all devices"
                  │
                  ▼
          Verify (OTP or Password)
                  │
                  ▼
          Revoke ALL tokens (including current)
                  │
                  ▼
          → Login screen

Session Management Screen

┌─────────────────────────────────────┐
│      Active Sessions 🔒             │
├─────────────────────────────────────┤
│                                     │
│   📱 iPhone 14 Pro                  │
│      Dar es Salaam • Active now    │
│      This device              [●]  │
│                                     │
│   💻 Chrome on Windows              │
│      Nairobi • 2 hours ago         │
│                        [Sign out]  │
│                                     │
│   📱 Samsung Galaxy S23             │
│      Mombasa • Yesterday           │
│                        [Sign out]  │
│                                     │
├─────────────────────────────────────┤
│                                     │
│   [Sign out other devices]         │
│                                     │
│   [Sign out all devices] ⚠️         │
│                                     │
└─────────────────────────────────────┘

Sign Out Endpoints

Endpoint Purpose
POST /auth/sign-out Current device
POST /auth/sign-out-others All except current
POST /auth/sign-out-all Everything
DELETE /auth/sessions/{id} Specific session

18. Industry Comparison

NextGate vs Major Platforms

Feature NextGate Instagram Twitter/X WhatsApp Banking Apps
Passwordless default ✅ Yes ❌ No ❌ No ✅ Yes 🟡 Some
Hardware-bound keys ✅ Yes ❌ No ❌ No ❌ No ✅ Yes
Risk-based auth ✅ Yes 🟡 Basic 🟡 Basic ❌ No ✅ Yes
Device trust ✅ Advanced 🟡 Basic 🟡 Basic 🟡 Basic ✅ Advanced
Impossible travel detection ✅ Yes 🟡 Limited 🟡 Limited ❌ No ✅ Yes
Session management ✅ Full ✅ Full ✅ Full 🟡 Limited ✅ Full
Age verification ✅ Tiered 🟡 Basic 🟡 Basic ❌ No ✅ Yes
Username change limits ✅ Smart 🟡 14 days 🟡 Limited ❌ N/A ❌ N/A
2FA options ✅ OTP ✅ OTP/App 💰 Paid ❌ No ✅ Multiple

Where We Stand

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   NextGate Auth vs Industry                                 │
│                                                             │
│   Social Apps (Instagram, Twitter):     AHEAD ✅            │
│   Messaging Apps (WhatsApp, Telegram):  EQUAL 🟡            │
│   Banking/Fintech:                      EQUAL 🟡            │
│   Big Tech (Google, Apple):             BEHIND ❌           │
│                                                             │
│   For Social Commerce Platform:         EXCELLENT 🎯        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Our Advantages

Over Advantage
Instagram/Twitter Hardware-bound device keys, passwordless default
WhatsApp Multi-identifier login, risk scoring, age gates
Basic apps Challenge-response auth, impossible travel detection

Future Improvements (v2)

Feature Impact Effort
Passkeys/WebAuthn +0.5 rating Medium
Backup codes +0.2 rating Low
Breach monitoring +0.2 rating Low
ML anomaly detection +0.3 rating High

Rating: 8.5/10 ⭐

Verdict: Enterprise-grade auth for a social commerce platform. Better than most social apps, equal to fintech.


Version: 1.0
Status: Ready for implementation

PONA AUTH V3

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


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

{
  "sub": "su_uuid",
  "flags": {
    "primaryComplete": true,
    "username": false,
    "email": false,
    "profilePic": false,
    "interests": false,
    "bio": false
  },
  "exp": "2026-04-01T12:00:00Z"
}

Resource Permission Matrix

Feature Needs Primary Needs Secondary
Browse events / listings ❌ No auth
React / like nothing extra
Buy ticket or product nothing extra
Share listing nothing extra
Comment publicly username
Follow someone username
Send a message username
Create an event username + email
Open a shop username + email
Sell a product username + email
Withdraw money username + email + profilePic
Age-restricted content ✅ must be 18+ nothing extra

Secondary Field Priority Order

Backend returns missing fields one at a time in this order. User never sees all missing fields at once.

1 — username      (needed for almost all social features)
2 — email         (needed for commerce and trust)
3 — profilePic    (needed for high-trust actions)
4 — bio           (rarely hard-required)
5 — interests     (feed personalization, almost never hard-required)

Auth Method Validation

Every auth endpoint validates the user has the method they are trying to use.

Endpoint Validation
/auth/login/password Account must have password set
/auth/login/oauth Google Google must be linked to this account
/auth/login/oauth Apple Apple must be linked to this account
/auth/password/forgot/initiate Account must have password set
/auth/passwordless/channels Always allowed
/auth/start OTP Always allowed — passwordless available to everyone

OTP Channel Selection

Passwordless users with email set can choose where to receive their OTP. Frontend never passes the raw email or phone — only the channel type enum.

Channel Availability Rules

Channel Available When
PHONE Always — phone is primary, always verified
EMAIL Only when email is set AND verified on the account

Action Codes — Complete Reference

Action Code What Frontend Does
REGISTER New user — show registration intro
CONTINUE_ONBOARDING Returning user, primary incomplete — resume
LOGIN Account ready — show auth method options
RESTART_AUTH Token expired — back to phone entry
SELECT_CHANNEL Multiple OTP channels — show picker
PROCEED_TO_OTP Single channel only — skip picker, go straight to OTP
USE_OTP Wrong auth method chosen — switch to OTP
RETRY_OTP Wrong OTP — error on same screen
RESEND_OTP OTP expired — activate resend
WAIT Rate limited — show countdown
ACCOUNT_BLOCKED Under 13 — show blocked screen
COLLECT_USERNAME Username needed
COLLECT_EMAIL Email needed — submit then OTP verify
COLLECT_PROFILE_PIC Profile picture needed
COLLECT_INTERESTS Interests needed
COLLECT_BIO Bio needed
PROCEED All steps done — retry original action

Response Shapes

Success

{
  "success": true,
  "message": "Human readable message",
  "action": "NEXT_ACTION_OR_NULL",
  "data": { }
}

Error — HTTP 422

{
  "success": false,
  "message": "Human readable message",
  "action": "NEXT_ACTION_CODE",
  "context": "what_user_was_trying_to_do",
  "data": { }
}

Response Examples

/auth/check — New User

{
  "success": true,
  "message": "Phone number not registered",
  "action": "REGISTER",
  "data": { "exists": false, "checkToken": null }
}

/auth/check — Existing User Ready

{
  "success": true,
  "message": "Welcome back",
  "action": "LOGIN",
  "data": {
    "exists": true,
    "checkToken": "eyJ...",
    "primaryComplete": true,
    "maskedPhone": "••• ••• ••78",
    "authMethods": {
      "passwordless": true,
      "password": true,
      "google": true,
      "apple": false
    }
  }
}

/auth/check — Primary Incomplete

{
  "success": true,
  "message": "Continue setting up your account",
  "action": "CONTINUE_ONBOARDING",
  "data": {
    "exists": true,
    "checkToken": "eyJ...",
    "primaryComplete": false,
    "maskedPhone": "••• ••• ••78"
  }
}

/auth/passwordless/channels — Multiple Channels

{
  "success": true,
  "message": "Choose where to receive your code",
  "action": "SELECT_CHANNEL",
  "data": {
    "channels": [
      { "type": "PHONE", "masked": "••• ••• ••78", "isPrimary": true },
      { "type": "EMAIL", "masked": "j••••@g••••.com", "isPrimary": false }
    ]
  }
}

/auth/passwordless/channels — Single Channel Only

{
  "success": true,
  "message": "Sending code to your phone",
  "action": "PROCEED_TO_OTP",
  "data": {
    "channels": [
      { "type": "PHONE", "masked": "••• ••• ••78", "isPrimary": true }
    ]
  }
}

/auth/start — OTP Sent

{
  "success": true,
  "message": "Verification code sent",
  "action": null,
  "data": {
    "tempToken": "eyJ...",
    "maskedDestination": "••• ••• ••78",
    "channel": "PHONE",
    "expiresInSeconds": 120,
    "resendAvailableAfterSeconds": 60
  }
}

/auth/verify — Primary Incomplete

{
  "success": true,
  "message": "Phone verified. Let us set up your account.",
  "action": "COLLECT_PRIMARY",
  "data": {
    "onboardingToken": "eyJ...",
    "nextStep": "name"
  }
}

/auth/verify — Primary Already Complete

{
  "success": true,
  "message": "Welcome back!",
  "action": null,
  "data": {
    "accessToken": "eyJ...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": false,
      "profilePic": false,
      "interests": false,
      "bio": false
    }
  }
}

/auth/onboarding/age — Blocked Underage

{
  "success": false,
  "message": "You must be at least 13 years old to use NextGate",
  "action": "ACCOUNT_BLOCKED",
  "context": "underage",
  "data": { "unblockDate": "2027-06-15" }
}

/auth/onboarding/age — Primary Complete

{
  "success": true,
  "message": "Welcome to NextGate!",
  "action": null,
  "data": {
    "accessToken": "eyJ...",
    "accountTier": "FULL",
    "onboarding": {
      "primaryComplete": true,
      "username": false,
      "email": false,
      "profilePic": false,
      "interests": false,
      "bio": false
    }
  }
}

OTP Wrong

{
  "success": false,
  "message": "Incorrect OTP code",
  "action": "RETRY_OTP",
  "context": "otp_verify",
  "data": { "attemptsRemaining": 2 }
}

OTP Expired

{
  "success": false,
  "message": "OTP has expired",
  "action": "RESEND_OTP",
  "context": "otp_expired",
  "data": { "resendAvailable": true, "resendCooldownSeconds": 0 }
}

Rate Limited

{
  "success": false,
  "message": "Too many attempts. Please wait.",
  "action": "WAIT",
  "context": "rate_limited",
  "data": { "retryAfterSeconds": 120 }
}

Wrong Auth Method

{
  "success": false,
  "message": "This account does not use password login",
  "action": "USE_OTP",
  "context": "password_login",
  "data": { "availableMethods": ["passwordless", "google"] }
}

Secondary Gate — Multiple Missing

{
  "success": false,
  "message": "A couple of things needed before you can create events",
  "action": "COLLECT_USERNAME",
  "context": "create_event",
  "data": {
    "currentMissing": "username",
    "allMissing": ["username", "email"],
    "stepsRemaining": 2
  }
}

Secondary Step Done — Next Signalled

{
  "success": true,
  "message": "Username set. One more step.",
  "action": "COLLECT_EMAIL",
  "context": "create_event",
  "data": {
    "accessToken": "eyJ...",
    "nextMissing": "email",
    "stepsRemaining": 1,
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": false,
      "profilePic": false,
      "interests": false,
      "bio": false
    }
  }
}

All Secondary Done — Proceed

{
  "success": true,
  "message": "All done. Creating your event now.",
  "action": "PROCEED",
  "context": "create_event",
  "data": {
    "accessToken": "eyJ...",
    "stepsRemaining": 0,
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": true,
      "profilePic": false,
      "interests": false,
      "bio": false
    }
  }
}

Forgot Password — Reset Complete

{
  "success": true,
  "message": "Password updated. All other sessions signed out.",
  "action": null,
  "data": { "accessToken": "eyJ..." }
}

Flow Diagrams

FLOW 1 — App Open with Stored Accounts

  ┌─────────────────────────────────────────────────────┐
  │  App opens                                          │
  └──────────────────────┬──────────────────────────────┘
                         │
                         ▼
           Read device secure storage
           for stored accounts list
                         │
              ┌──────────┴──────────┐
              │                     │
         NO ACCOUNTS           ACCOUNTS FOUND
              │                     │
              ▼                     ▼
     Show clean phone       Count stored accounts
     entry screen                   │
                           ┌────────┴────────┐
                           ONE              MULTIPLE
                           │                 │
                           ▼                 ▼
                   Auto-call          Show account
                   /auth/check        picker screen
                   in background      User taps one
                           │                 │
                           └────────┬────────┘
                                    ▼
                           /auth/check called
                           for that identifier
                                    │
                                    ▼
                           Show personalized
                           welcome screen with
                           auth method buttons

FLOW 2 — Auth Check (Entry Point)

  ┌─────────────────────────────────────────────────────┐
  │  POST /auth/check                                   │
  │  { "identifier": "+255712345678" }                  │
  └──────────────────────┬──────────────────────────────┘
                         │
                         ▼
           Valid phone format?
              │
   ┌──────────┴──────────┐
   NO                   YES
   │                     │
   ▼                     ▼
  422               Look up in database
  Invalid                │
  phone         ┌────────┴────────┐
                │                 │
           NOT FOUND           FOUND
                │                 │
                ▼                 ▼
       action: REGISTER    Phone verified?
       checkToken: null    ┌──────┴──────┐
                           NO            YES
                           │              │
                           ▼              ▼
                    Release phone  Primary complete?
                    from orphan    ┌──────┴──────┐
                    action: REGISTER NO           YES
                                   │              │
                                   ▼              ▼
                            action:        action: LOGIN
                            CONTINUE_      authMethods
                            ONBOARDING     returned
                                   │              │
                                   └──────┬───────┘
                                          ▼
                                  checkToken issued
                                  containing { identifier }
                                  stored to device on success

FLOW 3 — New User Registration

  action: REGISTER from /auth/check
  ............................................
  ┌─────────────────────────────────────────────────────┐
  │  POST /auth/start                                   │
  │  { "checkToken": "eyJ...", "channel": "PHONE" }     │
  └──────────────────────┬──────────────────────────────┘
                         │
                         ▼
           Phone extracted from checkToken
           Partial account created
           OTP sent via SMS
           tempToken issued
                         │
                         ▼
  ┌─────────────────────────────────────────────────────┐
  │  POST /auth/verify                                  │
  │  { "tempToken": "eyJ...", "otp": "123456" }         │
  └──────────────────────┬──────────────────────────────┘
                         │
                         ▼
            Phone verified
            Primary incomplete
            ONBOARDING TOKEN issued
            App locked to primary screens
                         │
              ┌──────────┴──────────┐
              ▼                     ▼
    POST /auth/             POST /auth/
    onboarding/name         onboarding/age
    { firstName,            { birthDate }
      lastName }                  │
          │                       ▼
          ▼               Under 13? → BLOCKED
    New onboarding          13-17 → RESTRICTED
    token returned          18+   → FULL
    Continue to age               │
                                  ▼
                         PRIMARY COMPLETE
                         ACCESS TOKEN issued
                         Identifier + name + avatar
                         saved to device storage
                         User lands on feed ✓

FLOW 4 — Existing User, Passwordless Login

  action: LOGIN, authMethods.passwordless: true
  User picks OTP option
  ............................................
  ┌─────────────────────────────────────────────────────┐
  │  POST /auth/passwordless/channels                   │
  │  { "checkToken": "eyJ..." }                         │
  └──────────────────────┬──────────────────────────────┘
                         │
                         ▼
           Backend checks account channels
                         │
              ┌──────────┴──────────┐
              │                     │
        ONE CHANNEL          MULTIPLE CHANNELS
        (phone only)         (phone + email)
              │                     │
              ▼                     ▼
       action:              action: SELECT_CHANNEL
       PROCEED_TO_OTP       Show channel picker
       Skip picker           User picks PHONE or EMAIL
              │                     │
              └──────────┬──────────┘
                         ▼
  ┌─────────────────────────────────────────────────────┐
  │  POST /auth/start                                   │
  │  { "checkToken": "eyJ...", "channel": "PHONE" }     │
  │  or { "checkToken": "eyJ...", "channel": "EMAIL" }  │
  └──────────────────────┬──────────────────────────────┘
                         │
                         ▼
           Backend extracts actual phone or email
           internally from account
           Sends OTP to chosen channel
           tempToken issued
                         │
                         ▼
  POST /auth/verify { tempToken, otp }
                         │
                         ▼
            OTP valid. Primary complete.
            ACCESS TOKEN issued.
            Device storage entry updated.
            User lands on feed ✓

FLOW 5 — Existing User, Password Login

  action: LOGIN, authMethods.password: true
  User picks password option
  ............................................
  ┌─────────────────────────────────────────────────────┐
  │  POST /auth/login/password                          │
  │  { "checkToken": "eyJ...", "password": "..." }      │
  └──────────────────────┬──────────────────────────────┘
                         │
                         ▼
           Account found from checkToken
                         │
              ┌──────────┴──────────┐
              NO PASSWORD           HAS PASSWORD
                   │                     │
                   ▼                     ▼
             422              Password verified
             action: USE_OTP  Risk assessed
             availableMethods          │
             returned          ┌───────┴───────┐
                               │               │
                          KNOWN DEVICE   UNKNOWN DEVICE
                               │               │
                               ▼               ▼
                        ACCESS TOKEN    Device OTP sent
                        issued          Verify device
                        directly        ACCESS TOKEN issued

FLOW 6 — OAuth Login

  action: LOGIN, authMethods.google: true
  User picks Google
  ............................................
  ┌─────────────────────────────────────────────────────┐
  │  POST /auth/login/oauth                             │
  │  { "checkToken": "eyJ...",                          │
  │    "provider": "GOOGLE", "code": "..." }            │
  └──────────────────────┬──────────────────────────────┘
                         │
                         ▼
           Account found from checkToken
           Google linked to account?
                         │
              ┌──────────┴──────────┐
              NO                   YES
              │                     │
              ▼                     ▼
        422               Google identity confirmed
        action: USE_OTP   Profile data pre-filled
        availableMethods  from Google
        returned                   │
                          Primary complete?
                          ┌────────┴────────┐
                          NO               YES
                          │                 │
                          ▼                 ▼
                  ONBOARDING TOKEN   ACCESS TOKEN
                  collect age        issued ✓

FLOW 7 — Forgot Password

  Only shown when authMethods.password: true
  ............................................
  ┌─────────────────────────────────────────────────────┐
  │  POST /auth/password/forgot/initiate                │
  │  { "checkToken": "eyJ..." }                         │
  └──────────────────────┬──────────────────────────────┘
                         │
                         ▼
           Account has password?
              │
   ┌──────────┴──────────┐
   NO                   YES
   │                     │
   ▼                     ▼
  422               OTP sent to phone
  action: USE_OTP   tempToken issued
                         │
                         ▼
  POST /auth/password/forgot/verify-otp
  { tempToken, otp }
                         │
                         ▼
              OTP verified
              resetToken issued (10 mins, single use)
                         │
                         ▼
  POST /auth/password/forgot/reset
  { resetToken, newPassword, confirmPassword }
                         │
                         ▼
              Password updated
              All other sessions revoked
              ACCESS TOKEN issued
              User logged in ✓

FLOW 8 — Secondary Onboarding (Progressive)

  User tries to create an event
  Needs: username + email
  username: false ← first missing
  email:    false
  ............................................
  422 from resource guard
  action: COLLECT_USERNAME
  allMissing: ["username", "email"]
  stepsRemaining: 2
  ............................................
  Frontend: "2 steps — Step 1 of 2"

  POST /onboarding/username
  Bearer <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

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

Being Added

Staying Exactly as They Are


Security Notes

PONA AUTH V3

NextGate PONA Auth — EndPoint Doc (ACTIVE)

Author: Josh S. Sakweli, Backend Lead — QBIT SPARK CO LIMITED
Last Updated: 2026-04-19
Version: v1.2
Base URL: https://your-api-domain.com/api/v1

For more details on the full flow design: PONA Auth v3 Design Doc


What is PONA Auth?

Progressive · Onboarding · Native · Access

PONA Auth is NextGate's unified, phone-first authentication system. It replaces all legacy auth flows with a single coherent pipeline. Core philosophy:

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

{
  "success": true,
  "httpStatus": "OK",
  "message": "Human-readable message",
  "action": "ACTION_CODE",
  "action_time": "2026-04-03T10:30:45",
  "data": {}
}

Error response

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2026-04-03T10:30:45",
  "data": "Error description"
}

Action codes

Code Meaning Next step
REGISTER Phone not found — new user Show registration UI, proceed to channels
LOGIN Phone found — existing user Show login UI, proceed to channels
CONTINUE_ONBOARDING Phone found but primary incomplete Proceed to channels → onboarding
PROCEED_TO_OTP Only one channel available Skip channel picker, send OTP automatically
SELECT_CHANNEL Multiple channels available Show channel picker to user
COLLECT_PRIMARY OTP verified, primary data needed Show name + birthdate form
ACCOUNT_BLOCKED User is underage or blocked Show blocked message with unblock date
VERIFY_DEVICE Unknown device on password login Show device OTP verification

OTP Channels

OTP can be delivered via the following channels. Not all channels are available in every situation — the server enforces the rules.

Available channel values

Value Description User selectable
SMS OTP delivered via SMS
WHATSAPP OTP delivered via WhatsApp
SMS_AND_WHATSAPP OTP sent to both SMS and WhatsApp simultaneously
EMAIL OTP delivered via email

SMS_AND_WHATSAPP fires both sends in parallel on the server. The user gets the OTP on both channels at the same time. If one channel fails, the other still delivers.

EMAIL_AND_WHATSAPP, EMAIL_AND_SMS, ALL_CHANNELS are internal server-side values. Never send these from the client — they will be rejected.

Channel rules by purpose

Channel New user (registration) Existing user (login)
SMS
WHATSAPP
SMS_AND_WHATSAPP
EMAIL ❌ not allowed ✅ only if account has a verified email

Channel request examples

Send via SMS only:

{ "channel": "SMS" }

Send via WhatsApp only:

{ "channel": "WHATSAPP" }

Send via both SMS and WhatsApp at the same time:

{ "channel": "SMS_AND_WHATSAPP" }

Send via email (login only, verified email required):

{ "channel": "EMAIL" }

Shared Objects

UserInfo

Returned by /auth/verify-otp and /auth/onboarding/primary once the user is identified. Frontend devs should persist this in local storage for display use (profile header, greetings, etc.).

{
  "displayName": "Joshua Sakweli",
  "phone": "+255745051250",
  "maskedPhone": "••• ••• ••50",
  "avatarUrl": "https://cdn.example.com/avatars/uuid.jpg"
}
Field Type Description
displayName string | null First + last name. Null until primary onboarding is complete.
phone string Full unmasked phone in international format. Always present. Safe to store — it is the user's own number, just verified via OTP.
maskedPhone string Masked phone for visible UI display (e.g. "••• ••• ••50"). Always present.
avatarUrl string | null URL of the user's profile picture. Null until a profile picture is uploaded.

Storage guidance: phone, maskedPhone, displayName, and avatarUrl are display data — localStorage is fine. Do not store tokens in localStorage.


HTTP Method Badges


Endpoints


1. Check Phone

Purpose: Entry point for every auth flow. Checks if a phone number is registered and returns a checkToken plus available auth methods.

Endpoint: POST {base_url}/auth/check

Access Level: 🌐 Public

Authentication: None

Request:

{
  "identifier": "+255745051250",
  "deviceId": "android-uuid-abc123"
}

Request Parameters:

Parameter Type Required Description Validation
identifier string Yes Phone number in international format Must match ^\+[1-9]\d{6,14}$
deviceId string Yes Unique device identifier from the client Non-empty

Response — New User:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Phone number not registered",
  "action": "REGISTER",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "exists": false,
    "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
    "primaryComplete": false,
    "maskedPhone": null,
    "authMethods": null
  }
}

Response — Existing User:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Welcome back",
  "action": "LOGIN",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "exists": true,
    "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
    "primaryComplete": true,
    "maskedPhone": "••• ••• ••50",
    "authMethods": {
      "passwordless": true,
      "password": false,
      "google": true,
      "apple": false
    }
  }
}

Response — Existing User, Primary Incomplete:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Continue setting up your account",
  "action": "CONTINUE_ONBOARDING",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "exists": true,
    "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
    "primaryComplete": false,
    "maskedPhone": "••• ••• ••50",
    "authMethods": {
      "passwordless": true,
      "password": false,
      "google": false,
      "apple": false
    }
  }
}

Response Fields:

Field Description
exists Whether the phone is registered
checkToken Short-lived token to proceed. Always present.
primaryComplete Whether the user has completed name + birthdate setup
maskedPhone Masked phone for display. Null for new users.
authMethods.passwordless Always true
authMethods.password True if user has set a password
authMethods.google True if Google is linked
authMethods.apple True if Apple is linked

Frontend handling:

action = REGISTER
  → store checkToken in memory
  → do NOT show password field
  → do NOT show Google/Apple buttons
  → proceed to channel picker

action = LOGIN
  → store checkToken in memory
  → show Google button ONLY if authMethods.google = true
  → show Password button ONLY if authMethods.password = true
  → always show OTP button
  → proceed to channel picker

action = CONTINUE_ONBOARDING
  → same as LOGIN
  → user will be redirected to primary onboarding after OTP verify

Errors:


2. Get Passwordless Channels

Purpose: Returns the available OTP delivery channels for the user. Always returns SMS and WHATSAPP. EMAIL is returned only if the user has a verified email.

Endpoint: POST {base_url}/auth/passwordless/channels

Access Level: 🌐 Public

Authentication: None

This endpoint does NOT consume the checkToken.

Request:

{
  "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
  "deviceId": "android-uuid-abc123"
}

Request Parameters:

Parameter Type Required Description
checkToken string Yes Token from /auth/check
deviceId string Yes Must match the deviceId used in /auth/check

Response — Phone only:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Choose where to receive your code",
  "action": "SELECT_CHANNEL",
  "action_time": "2026-04-16T10:30:45",
  "data": {
    "channels": [
      { "channel": "SMS",      "masked": "••• ••• ••50", "isPrimary": true  },
      { "channel": "WHATSAPP", "masked": "••• ••• ••50", "isPrimary": false }
    ]
  }
}

Response — Phone + verified email:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Choose where to receive your code",
  "action": "SELECT_CHANNEL",
  "action_time": "2026-04-16T10:30:45",
  "data": {
    "channels": [
      { "channel": "SMS",      "masked": "••• ••• ••50",      "isPrimary": true  },
      { "channel": "WHATSAPP", "masked": "••• ••• ••50",      "isPrimary": false },
      { "channel": "EMAIL",    "masked": "j••••••@g••••.com", "isPrimary": false }
    ]
  }
}

This endpoint returns individual primitive channels only (SMS, WHATSAPP, EMAIL). The SMS_AND_WHATSAPP compound value is not returned here — the frontend constructs it when the user wants both.

Frontend handling:

Always show at least SMS and WHATSAPP.
If EMAIL is present, show it too.

Suggested UI:
  SMS       → "Text message to ••• ••• ••50"
  WHATSAPP  → "WhatsApp to ••• ••• ••50"
  EMAIL     → "Email to j••••••@g••••.com"

You can also show a "Send to both SMS and WhatsApp" option — 
send SMS_AND_WHATSAPP as the channel value in /auth/passwordless-start.

User taps their choice, then call /auth/passwordless-start with that channel value.

Errors:


3. Start Passwordless OTP

Purpose: Sends an OTP to the chosen channel and returns a tempToken for the verify step. Consumes the checkToken.

Endpoint: POST {base_url}/auth/passwordless-start

Access Level: 🌐 Public

Authentication: None

Request — SMS only:

{
  "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
  "channel": "SMS",
  "deviceId": "android-uuid-abc123"
}

Request — WhatsApp only:

{
  "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
  "channel": "WHATSAPP",
  "deviceId": "android-uuid-abc123"
}

Request — Both SMS and WhatsApp simultaneously:

{
  "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
  "channel": "SMS_AND_WHATSAPP",
  "deviceId": "android-uuid-abc123"
}

Request — Email (login only, verified email required):

{
  "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
  "channel": "EMAIL",
  "deviceId": "android-uuid-abc123"
}

Request Parameters:

Parameter Type Required Description Validation
checkToken string Yes Token from /auth/check Non-empty
channel enum Yes Where to send OTP SMS, WHATSAPP, SMS_AND_WHATSAPP, EMAIL
deviceId string Yes Must match deviceId from /auth/check Non-empty

Channel rules:

Channel New user (registration) Existing user (login)
SMS
WHATSAPP
SMS_AND_WHATSAPP
EMAIL ✅ only if verified email exists

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Verification code sent",
  "action_time": "2026-04-16T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiJ9...",
    "maskedDestination": "••• ••• ••50",
    "channel": "SMS_AND_WHATSAPP",
    "expiresInSeconds": 120,
    "resendAvailableAfterSeconds": 60
  }
}

Response Fields:

Field Description
tempToken Carry this to /auth/verify-otp. Store in memory only.
maskedDestination Show to the user so they know where OTP was sent
channel The channel used — display appropriate message
expiresInSeconds OTP valid for this many seconds
resendAvailableAfterSeconds Wait this long before enabling resend

Frontend handling:

On success:
  → store tempToken in memory
  → show OTP input screen
  → display message based on channel:
      SMS              → "Code sent to ••• ••• ••50 via SMS"
      WHATSAPP         → "Code sent to ••• ••• ••50 via WhatsApp"
      SMS_AND_WHATSAPP → "Code sent to ••• ••• ••50 via SMS and WhatsApp"
      EMAIL            → "Code sent to j••••••@g••••.com"
  → start countdown timer using resendAvailableAfterSeconds
  → enable resend button when timer hits 0

On resend:
  → channel is locked to the original choice
  → resend always goes to the same channel(s)
  → to switch channel, go back to the channel picker and restart the flow

Errors:


4. Verify OTP

Purpose: Validates the OTP and returns either an accessToken (returning user, primary complete) or an onboardingToken (new or incomplete user).

Endpoint: POST {base_url}/auth/verify-otp

Access Level: 🌐 Public

Authentication: None

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiJ9...",
  "otp": "482910",
  "deviceName": "Josh's Pixel 4a",
  "platform": "ANDROID"
}

Request Parameters:

Parameter Type Required Description Validation
tempToken string Yes Token from /auth/passwordless-start Non-empty
otp string Yes 6-digit code Exactly 6 numeric digits
deviceName string No Human-readable device name Optional
platform string No Client platform ANDROID, IOS, WEB

Response — Primary Complete:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Welcome back",
  "action": null,
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
    "onboardingToken": null,
    "primaryComplete": true,
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": false,
      "profilePic": false,
      "interests": false,
      "bio": false
    },
    "user": {
      "displayName": "Joshua Sakweli",
      "phone": "+255745051250",
      "maskedPhone": "••• ••• ••50",
      "avatarUrl": null
    }
  }
}

Response — Primary Incomplete:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Phone verified. Let us set up your account.",
  "action": "COLLECT_PRIMARY",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "accessToken": null,
    "refreshToken": null,
    "onboardingToken": "eyJhbGciOiJIUzI1NiJ9...",
    "primaryComplete": false,
    "onboarding": {
      "primaryComplete": false,
      "username": false,
      "email": false,
      "profilePic": false,
      "interests": false,
      "bio": false
    },
    "user": {
      "displayName": null,
      "phone": "+255745051250",
      "maskedPhone": "••• ••• ••50",
      "avatarUrl": null
    }
  }
}

Frontend handling:

primaryComplete = true:
  → store accessToken in memory
  → store refreshToken in HttpOnly cookie (web) or Keychain (mobile)
  → discard tempToken
  → navigate to home
  → check onboarding flags for secondary prompts

primaryComplete = false:
  → store onboardingToken in memory
  → discard tempToken
  → navigate to primary onboarding screen

Errors:


5. Resend OTP

Purpose: Resends the OTP to the same channel and destination as the original send. Channel cannot be changed on resend. Rate limited to 5 attempts per session with a 60-second cooldown.

Endpoint: POST {base_url}/auth/resend-otp

Access Level: 🌐 Public

Authentication: None

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiJ9..."
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP resent successfully",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiJ9...",
    "maskedIdentifier": "••• ••• ••50",
    "remainingAttempts": 4,
    "expiresIn": 900
  }
}

Frontend handling:

On success:
  → replace tempToken in memory with the new one from response
  → show "Code resent" confirmation
  → reset the countdown timer to resendAvailableAfterSeconds
  → disable resend button again

On 400 — cooldown active:
  → show "Please wait X seconds"
  → do not clear the OTP input

On 400 — max attempts:
  → show "Too many attempts. Please start over."
  → clear tempToken from memory
  → navigate back to channel picker

Channel switching:
  → NOT possible via resend
  → user must go back to channel picker and call /auth/passwordless-start again

Errors:


6. Primary Onboarding

Purpose: Collects name and date of birth. Completes primary onboarding and issues the first accessToken.

Endpoint: POST {base_url}/auth/onboarding/primary

Access Level: 🌐 Public

Authentication: None (uses onboardingToken in body)

Request:

{
  "onboardingToken": "eyJhbGciOiJIUzI1NiJ9...",
  "firstName": "Joshua",
  "lastName": "Sakweli",
  "birthDate": "1995-06-15"
}

Request Parameters:

Parameter Type Required Description Validation
onboardingToken string Yes Token from /auth/verify-otp Non-empty
firstName string Yes User's first name 1–50 characters
lastName string Yes User's last name 1–50 characters
birthDate string Yes Date of birth YYYY-MM-DD, must be in the past

Response — Normal User:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Welcome to NextGate!",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
    "accountTier": "FULL",
    "onboarding": {
      "primaryComplete": true,
      "username": false,
      "email": false,
      "profilePic": false,
      "interests": false,
      "bio": false
    },
    "blocked": false,
    "unblockDate": null,
    "user": {
      "displayName": "Joshua Sakweli",
      "phone": "+255745051250",
      "maskedPhone": "••• ••• ••50",
      "avatarUrl": null
    }
  }
}

Response — Underage User:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account blocked",
  "action": "ACCOUNT_BLOCKED",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "accessToken": null,
    "refreshToken": null,
    "accountTier": null,
    "onboarding": null,
    "blocked": true,
    "unblockDate": "2026-09-15"
  }
}

Response Fields:

Field Description
accessToken Bearer token. Store in memory. Null if blocked.
refreshToken Rotation token. Store securely. Null if blocked.
accountTier FULL (18+), RESTRICTED (13–17), MINOR (under 13 — blocked)
blocked True if user is underage
unblockDate Date when user turns 13. Show to user.

Frontend handling:

blocked = false:
  → store accessToken in memory
  → store refreshToken securely
  → discard onboardingToken
  → navigate to home
  → check onboarding flags for secondary prompts

blocked = true:
  → do NOT store any tokens
  → show age restriction screen with unblockDate
  → do NOT allow navigation into the app

accountTier = RESTRICTED:
  → user is 13–17
  → restrict features per your tier config

Errors:


7. Password Login

Purpose: Authenticates a user with phone + password. May require device verification if the device is unknown or risk is high.

Endpoint: POST {base_url}/auth/login/password

Access Level: 🌐 Public

Authentication: None

Request:

{
  "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
  "password": "MySecurePassword123",
  "deviceId": "android-uuid-abc123",
  "deviceName": "Josh's Pixel 4a",
  "platform": "ANDROID"
}

Request Parameters:

Parameter Type Required Description Validation
checkToken string Yes Token from /auth/check Non-empty
password string Yes User's password Non-empty
deviceId string Yes Device identifier Non-empty
deviceName string No Human-readable device name Optional
platform string No Client platform ANDROID, IOS, WEB

Response — Known Device:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": false,
      "profilePic": false,
      "interests": true,
      "bio": false
    },
    "requiresDeviceVerification": false,
    "deviceVerificationToken": null,
    "maskedDestination": null
  }
}

Response — Unknown / High Risk Device:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device verification required",
  "action": "VERIFY_DEVICE",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "accessToken": null,
    "refreshToken": null,
    "requiresDeviceVerification": true,
    "deviceVerificationToken": "eyJhbGciOiJIUzI1NiJ9...",
    "maskedDestination": "••• ••• ••50"
  }
}

Frontend handling:

requiresDeviceVerification = false:
  → store accessToken in memory
  → store refreshToken securely
  → navigate to home

requiresDeviceVerification = true:
  → store deviceVerificationToken in memory
  → show OTP input with maskedDestination
  → call POST /api/v1/account/device/verify with the OTP
  → on success you get accessToken + refreshToken

Errors:


8. OAuth Login

Purpose: Authenticates a user via Google or Apple. Only available if the provider was previously linked to the account.

Endpoint: POST {base_url}/auth/login/oauth

Access Level: 🌐 Public

Authentication: None

Request:

{
  "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
  "provider": "GOOGLE",
  "idToken": "google-id-token-from-client-sdk",
  "deviceId": "android-uuid-abc123",
  "deviceName": "Josh's Pixel 4a",
  "platform": "ANDROID",
  "state": "optional-state-string"
}

Request Parameters:

Parameter Type Required Description Validation
checkToken string Yes Token from /auth/check Non-empty
provider string Yes OAuth provider GOOGLE, APPLE
idToken string Yes ID token from Google/Apple client SDK Non-empty
deviceId string Yes Device identifier Non-empty
deviceName string No Human-readable device name Optional
platform string No Client platform ANDROID, IOS, WEB
state string No Opaque state value passed back in response Optional

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": true,
      "profilePic": false,
      "interests": true,
      "bio": false
    },
    "state": "optional-state-string"
  }
}

Frontend handling:

Before calling this endpoint:
  → check authMethods.google from /auth/check response
  → ONLY show Google button if google = true
  → ONLY show Apple button if apple = true

On success:
  → store accessToken in memory
  → store refreshToken securely
  → navigate to home

Errors:


9. Forgot Password — Initiate

Purpose: Starts the forgot password flow. Sends an OTP to the user's phone via SMS. Does NOT consume the checkToken.

Endpoint: POST {base_url}/auth/password/forgot/initiate

Access Level: 🌐 Public

Authentication: None

Request:

{
  "checkToken": "eyJhbGciOiJIUzI1NiJ9...",
  "deviceId": "android-uuid-abc123"
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Password reset code sent to your phone",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiJ9...",
    "resetToken": null,
    "maskedPhone": "••• ••• ••50",
    "accessToken": null,
    "expiresInSeconds": 120
  }
}

Frontend handling:

→ store tempToken in memory
→ show OTP input screen
→ display maskedPhone
→ proceed to /auth/password/forgot/verify-otp

Errors:


10. Forgot Password — Verify OTP

Purpose: Verifies the OTP and issues a short-lived resetToken.

Endpoint: POST {base_url}/auth/password/forgot/verify-otp

Access Level: 🌐 Public

Authentication: None

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiJ9...",
  "otp": "482910"
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Identity confirmed. Set your new password.",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "tempToken": null,
    "resetToken": "eyJhbGciOiJIUzI1NiJ9...",
    "maskedPhone": null,
    "accessToken": null,
    "expiresInSeconds": 0
  }
}

Frontend handling:

→ discard tempToken from memory
→ store resetToken in memory
→ navigate to new password input screen

Errors:


11. Forgot Password — Reset

Purpose: Sets the new password. Revokes all existing sessions and issues a fresh accessToken.

Endpoint: POST {base_url}/auth/password/forgot/reset

Access Level: 🌐 Public

Authentication: None

Request:

{
  "resetToken": "eyJhbGciOiJIUzI1NiJ9...",
  "newPassword": "MyNewSecurePassword456",
  "confirmPassword": "MyNewSecurePassword456"
}

Request Parameters:

Parameter Type Required Description Validation
resetToken string Yes Token from verify OTP step Non-empty
newPassword string Yes New password Min 8 characters
confirmPassword string Yes Must match newPassword Non-empty

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Password updated. All other sessions signed out.",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
    "resetToken": null,
    "maskedPhone": null,
    "expiresInSeconds": 0
  }
}

Frontend handling:

→ discard resetToken from memory
→ store accessToken in memory
→ clear any existing refreshToken from storage
→ navigate to home
→ show "Password updated successfully"

Errors:


12. Refresh Token

Purpose: Exchanges a refresh token for a new access + refresh token pair. Old refresh token is invalidated immediately (rotation).

Endpoint: POST {base_url}/auth/token/refresh

Access Level: 🌐 Public

Authentication: None

Request:

{
  "refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Token refreshed",
  "action_time": "2026-04-03T10:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
    "expiresIn": 3600
  }
}

Frontend handling:

Call this when:
  → accessToken is expired (401 on a protected request)
  → proactively before expiry (check exp claim in JWT)

On success:
  → replace accessToken in memory
  → replace refreshToken in secure storage
  → retry the original failed request

On 401:
  → clear all tokens
  → redirect to login

Errors:


13. Revoke Token

Purpose: Logs out the user by revoking their refresh token.

Endpoint: POST {base_url}/auth/token/revoke

Access Level: 🌐 Public

Authentication: None

Request:

{
  "refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Token revoked successfully",
  "action_time": "2026-04-03T10:30:45",
  "data": null
}

Frontend handling:

On logout:
  → call this endpoint with the stored refreshToken
  → clear accessToken from memory
  → clear refreshToken from secure storage
  → redirect to login

If call fails (network error):
  → still clear tokens locally
  → user is effectively logged out on the client

Quick Reference — Full Auth Flow

1. POST /auth/check
   → phone + deviceId → checkToken + action

2. POST /auth/passwordless/channels   (does not consume checkToken)
   → returns available channels: SMS, WHATSAPP, and optionally EMAIL

3. POST /auth/passwordless-start      (consumes checkToken)
   → channel (SMS | WHATSAPP | SMS_AND_WHATSAPP | EMAIL) → tempToken + OTP sent

4. POST /auth/verify-otp              (consumes tempToken)
   → otp → accessToken (returning user) or onboardingToken (new user)

5. POST /auth/onboarding/primary      (if onboardingToken received)
   → name + birthDate → accessToken issued

─── User is now logged in ───

6. Secondary onboarding (optional, progressive)
   → username, email, interests, bio, profile pic
   → each step returns new accessToken with updated onboarding flags

─── Token management ───

7. POST /auth/token/refresh   → rotate tokens silently
8. POST /auth/token/revoke    → logout

Error Handling Summary

HTTP Status When it happens What to do
400 BAD_REQUEST Invalid input, item exists, rate limit Show error message to user
401 UNAUTHORIZED Token expired or invalid Refresh token or redirect to login
403 FORBIDDEN Wrong OTP, wrong password, token mismatch Show specific error, let user retry
404 NOT_FOUND Account not found Show "Account not found"
422 UNPROCESSABLE_ENTITY Validation failed Show field-level errors
500 INTERNAL_SERVER_ERROR Server error Show generic error, retry
PONA AUTH V3

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:


Standard Response Format

All API responses follow a consistent structure using our Globe Response Builder pattern:

Success Response Structure

{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-09-23T10:30:45",
  "action": "COLLECT_EMAIL",
  "data": {}
}

Error Response Structure

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-09-23T10:30:45",
  "data": "Error description"
}

Standard Response Fields

Field Type Description
success boolean true for successful operations, false for errors
httpStatus string HTTP status name (OK, BAD_REQUEST, etc.)
message string Human-readable result description
action_time string ISO 8601 timestamp of the response
action string Next frontend action to perform (see action codes below)
data object/string Response payload or error detail

Action Codes

Code Meaning
COLLECT_USERNAME Username step is next
COLLECT_EMAIL Email step is next
COLLECT_PROFILE_PIC Profile picture step is next
COLLECT_INTERESTS Interests step is next
COLLECT_BIO Bio step is next
PROCEED All steps complete — onboarding is done

Standard Secondary Onboarding Response Fields

Most endpoints return a SecondaryOnboardingResponse. Its fields are:

Field Type Description
accessToken string Fresh JWT — store and use this for all subsequent requests
onboarding object Current completion state of all onboarding steps (see below)
nextMissing string Key name of the next incomplete step, or null if all done
stepsRemaining integer Number of steps still pending

onboarding Object Fields

Field Type Description
primaryComplete boolean Primary onboarding (name/phone/DOB) is done
username boolean Username has been set
email boolean Email has been linked and verified
profilePic boolean Profile picture has been uploaded
interests boolean Interests have been selected
bio boolean Bio has been written

Endpoints

1. Get Username Suggestions

Purpose: Returns up to 5 AI-generated username suggestions based on the user's first name, last name, and birth date.

Endpoint: GET {base_url}/username/suggestions

Access Level: 🔒 Protected (Authenticated user)

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer <access_token>

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Username suggestions",
  "action_time": "2026-04-27T10:30:45",
  "data": {
    "suggestions": [
      "john_sakweli",
      "johnsakweli99",
      "j_sakweli",
      "johnsak2004",
      "jsakweli_"
    ]
  }
}

Success Response Fields:

Field Description
suggestions Array of up to 5 available username strings

Error Response JSON Sample:

{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Account not found",
  "action_time": "2026-04-27T10:30:45",
  "data": "Account not found"
}

2. Set Username

Purpose: Sets a unique username for the account.

Endpoint: POST {base_url}/username

Access Level: 🔒 Protected (Authenticated user)

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer <access_token>
Content-Type string Yes application/json

Request JSON Sample:

{
  "username": "john_sakweli"
}

Request Body Parameters:

Parameter Type Required Description Validation
username string Yes Desired username Min: 3, Max: 30 characters. Must start with a letter. Only letters, numbers, and underscores allowed.

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Username set successfully",
  "action_time": "2026-04-27T10:30:45",
  "action": "COLLECT_EMAIL",
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": false,
      "profilePic": false,
      "interests": false,
      "bio": false
    },
    "nextMissing": "email",
    "stepsRemaining": 4
  }
}

Success Response Fields:

Field Description
accessToken Fresh JWT with updated onboarding claims — replace stored token
onboarding.username Now true
nextMissing Next recommended step key
stepsRemaining Steps left to complete

Error Response JSON Sample:

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Username is already taken",
  "action_time": "2026-04-27T10:30:45",
  "data": "Username is already taken"
}

Standard Error Types:


3. Set Bio

Purpose: Saves a short bio to the user's profile.

Endpoint: POST {base_url}/bio

Access Level: 🔒 Protected (Authenticated user)

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer <access_token>
Content-Type string Yes application/json

Request JSON Sample:

{
  "bio": "Event enthusiast, live music lover, always at the front row."
}

Request Body Parameters:

Parameter Type Required Description Validation
bio string Yes User's short profile bio Max: 160 characters. Cannot be blank.

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Bio saved",
  "action_time": "2026-04-27T10:30:45",
  "action": "PROCEED",
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": true,
      "profilePic": true,
      "interests": true,
      "bio": true
    },
    "nextMissing": null,
    "stepsRemaining": 0
  }
}

Standard Error Types:


4. Set Interests

Purpose: Saves the user's selected interest categories (minimum 3 required).

Endpoint: POST {base_url}/interests

Access Level: 🔒 Protected (Authenticated user)

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer <access_token>
Content-Type string Yes application/json

Request JSON Sample:

{
  "interestIds": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "7a1234bc-1234-4321-abcd-1234567890ab",
    "9c87654d-4321-1234-dcba-0987654321cd"
  ]
}

Request Body Parameters:

Parameter Type Required Description Validation
interestIds array of UUID Yes IDs of selected interest categories Min: 3 items. Must not be empty. Use the interests listing endpoint to get valid IDs.

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Interests saved",
  "action_time": "2026-04-27T10:30:45",
  "action": "COLLECT_BIO",
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": true,
      "profilePic": true,
      "interests": true,
      "bio": false
    },
    "nextMissing": "bio",
    "stepsRemaining": 1
  }
}

Standard Error Types:


5. Upload Profile Picture

Purpose: Uploads and stores the user's profile picture.

Endpoint: POST {base_url}/profile-pic

Access Level: 🔒 Protected (Authenticated user)

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer <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:

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


6. Initiate Email Linking (Custom)

Purpose: Sends a 6-digit OTP to the provided email address and returns a tempToken required for the verify step.

Endpoint: POST {base_url}/email/custom/initiate

Access Level: 🔒 Protected (Authenticated user)

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer <access_token>
Content-Type string Yes application/json

Request JSON Sample:

{
  "email": "john@example.com"
}

Request Body Parameters:

Parameter Type Required Description Validation
email string Yes Email address to link Must be a valid email format. Cannot be blank.

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Verification code sent to your email",
  "action_time": "2026-04-27T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJSUzI1NiJ9.temp...",
    "nextAction": "VERIFY_EMAIL"
  }
}

Success Response Fields:

Field Description
tempToken Short-lived token required as input to the verify step. Store this until OTP is submitted.
nextAction Always VERIFY_EMAIL — signals frontend to show OTP input screen

Standard Error Types:


7. Verify Email (Custom)

Purpose: Confirms the OTP sent to the user's email and marks the email step as complete.

Endpoint: POST {base_url}/email/custom/verify

Access Level: 🔒 Protected (Authenticated user)

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer <access_token>
Content-Type string Yes application/json

Request JSON Sample:

{
  "tempToken": "eyJhbGciOiJSUzI1NiJ9.temp...",
  "otp": "847291"
}

Request Body Parameters:

Parameter Type Required Description Validation
tempToken string Yes Token received from the initiate step Cannot be blank
otp string Yes 6-digit code sent to the user's email Exactly 6 digits, numeric only

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Email verified",
  "action_time": "2026-04-27T10:30:45",
  "action": "COLLECT_PROFILE_PIC",
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": true,
      "profilePic": false,
      "interests": false,
      "bio": false
    },
    "nextMissing": "profilePic",
    "stepsRemaining": 3
  }
}

Standard Error Types:


Endpoint: POST {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:

{
  "idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6..."
}

Request Body Parameters:

Parameter Type Required Description Validation
idToken string Yes Google ID token from the Google Sign-In SDK Cannot be blank. Must be a valid Google-issued token.

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Email linked via Google",
  "action_time": "2026-04-27T10:30:45",
  "action": "COLLECT_PROFILE_PIC",
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": true,
      "profilePic": false,
      "interests": false,
      "bio": false
    },
    "nextMissing": "profilePic",
    "stepsRemaining": 3
  }
}

Standard Error Types:


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