πŸ”’ Need secure & scalable cloud deployment with project report for college or research setup ?

Click here to Contact Us β†’

🐝 Made with ❀️ by Byte Bees

AI Study Tutor - Technical Documentation

AI-Powered Study Tutor Application with Retrieval-Augmented Generation (RAG) capabilities for document analysis and conversational learning.

πŸš€ Quick Setup

Prerequisites

Installation Steps

# 1. Clone the repository
git clone <repo-url>
cd AI-Study-Tutor

# 2. Frontend (cognify-frontend)
cd cognify-frontend && npm install && npm run dev
# Runs on http://localhost:5173

# 3. Node.js Backend (node-backend)
cd ../node-backend && npm install
cp .env.example .env  # Configure your .env
npm run dev
# Runs on http://localhost:4000

# 4. FastAPI Backend (ai-engine)
cd ../ai-engine

# Create virtual environment
python -m venv venv
source venv/bin/activate  # macOS/Linux
# Windows: venv\Scripts\activate

# Install and run
pip install -r requirements.txt
cp .env.example .env  # Configure your .env
uvicorn app.main:app --reload
# Runs on http://localhost:8000

MongoDB Atlas Setup (Recommended)

Why MongoDB Atlas?

Setup Steps:

  1. Go to mongodb.com/atlas and sign up
  2. Create a FREE cluster (M0 Sandbox)
  3. Create database user with password
  4. Whitelist IP: "Allow Access from Anywhere"
  5. Get connection string from "Connect" β†’ "Connect your application"
# Example connection string:
mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/rag_app

# Add to node-backend/.env:
MONGO_URI=mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/rag_app

Required .env Files

node-backend/.env

MONGO_URI=mongodb+srv://...
JWT_SECRET=your-secret
GOOGLE_CLIENT_ID=xxx
EMAIL_USER=email@zoho.in
EMAIL_PASSWORD=xxx

ai-engine/.env

LLM_PROVIDER=gemini
GOOGLE_API_KEY=your-key
JWT_SECRET=your-secret

See Section 5 for complete environment variable reference.

1. Technology Stack

1.1 Frontend (cognify-frontend)

Technology Purpose
React 19 Modern UI framework with latest features
TypeScript Type-safe JavaScript for scalable development
Vite Lightning-fast build tool and dev server
TailwindCSS 4 Utility-first CSS framework
Redux Toolkit State management with redux-persist
React Router v7 Client-side routing
Framer Motion Smooth animations and transitions
Mermaid Diagram rendering for concept maps
react-markdown Markdown rendering with remark-gfm
Ant Design UI component library

Why these choices?


1.2 Node.js Backend (node-backend)

Technology Purpose
Express.js Fast, minimalist web framework
TypeScript Type safety for backend code
MongoDB + Mongoose NoSQL database for flexible schemas
JWT (jsonwebtoken) Stateless authentication
AWS S3 SDK Cloud file storage integration
Multer File upload handling
bcryptjs Password hashing
Nodemailer Email services
Google Auth Library OAuth2 social login

Why these choices?


1.3 FastAPI Backend (ai-engine)

Technology Purpose
FastAPI High-performance async Python framework
Uvicorn ASGI server for async processing
ChromaDB Vector database for embeddings
Sentence Transformers Text embedding generation
PyMuPDF (fitz) PDF text extraction
OpenAI/DeepInfra/Gemini LLM provider integrations
BeautifulSoup4 Web scraping for URLs
boto3 AWS S3 integration
Pydantic Settings Configuration management

Why these choices?


2. Storage Configuration

The application supports dual storage providers with a unified abstraction layer:

2.1 Storage Provider Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Storage Abstraction β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ IStorageProvider Interface β”‚ β”‚ β”œβ”€β”€ uploadFile(key, content, contentType) β”‚ β”‚ β”œβ”€β”€ uploadPDF(workspaceId, fileName, buffer) β”‚ β”‚ β”œβ”€β”€ uploadText(workspaceId, title, content) β”‚ β”‚ β”œβ”€β”€ getSignedReadUrl(key, expiresIn) β”‚ β”‚ β”œβ”€β”€ deleteFile(key) β”‚ β”‚ β”œβ”€β”€ getFileContent(key) β”‚ β”‚ └── fileExists(key) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ LocalStorageProvider β”‚ β”‚ S3StorageProvider β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β€’ Filesystem-based β”‚ β”‚ β€’ AWS S3 bucket β”‚ β”‚ β€’ HTTP static serve β”‚ β”‚ β€’ Pre-signed URLs β”‚ β”‚ β€’ Development/Self- β”‚ β”‚ β€’ Scalable cloud β”‚ β”‚ hosted production β”‚ β”‚ β€’ CDN-ready β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2.2 Local Storage Provider

Configuration:

STORAGE_PROVIDER=local
LOCAL_STORAGE_PATH=../application-data

How it works:

  1. Files stored in application-data/ folder at project root
  2. Node.js serves as static file server via Express
  3. URLs return as {BACKEND_URL}/storage/{key}
  4. Path traversal protection with key sanitization

File organization:

application-data/
└── {userEmail}/
    └── workspaces/
        └── {workspaceId}/
            β”œβ”€β”€ pdfs/
            β”‚   └── {timestamp}-{filename}.pdf
            └── texts/
                └── {timestamp}-{title}.txt

2.3 AWS S3 Storage Provider

Configuration:

STORAGE_PROVIDER=s3
AWS_S3_BUCKET=your-bucket-name
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key

How it works:

  1. Files uploaded to S3 bucket with structured keys
  2. Pre-signed URLs generated for secure, time-limited access
  3. Supports CloudFront CDN for global distribution
  4. Same file organization pattern as local storage

S3 URL format:

https://{bucket}.s3.{region}.amazonaws.com/{key}

2.4 Switching Between Providers

Simply change the STORAGE_PROVIDER environment variable:

Provider Value Use Case
Local local Development, self-hosted deployments
AWS S3 s3 Production, scalable deployments

Both backends (Node.js and FastAPI) use the same abstraction, ensuring consistent file access across the stack.


3. Technical Flow & Architecture

3.1 High-Level Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Frontend (React + Vite) β”‚ β”‚ UI β†’ Redux Store β†’ API Client β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Node.js (Express) β”‚ β”‚ FastAPI (Python) β”‚ β”‚ β€’ Auth Service β”‚ β”‚ β€’ RAG Agent β”‚ β”‚ β€’ Workspace Service β”‚ β”‚ β€’ Document Processorβ”‚ β”‚ β€’ Storage Service β”‚ β”‚ β€’ Content Generator β”‚ β”‚ β€’ MongoDB β”‚ β”‚ β€’ ChromaDB β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–Ό β–Ό AWS S3 / Local LLM Providers File Storage (Gemini/OpenAI/DeepInfra)

3.2 Complete Workflow

Step 1: User Authentication (Node.js)

User β†’ POST /api/auth/login ↓ AuthService validates credentials (bcrypt) ↓ JWT token generated with user ID ↓ Token stored in client (localStorage) ↓ All subsequent requests include: Authorization: Bearer {token}

Step 2: Document Upload Flow

1. User uploads PDF/Text in frontend β”‚ β–Ό 2. Node.js receives file via Multer POST /api/workspaces/{id}/sources β”‚ β–Ό 3. StorageService determines provider β”œβ”€β”€ Local: Write to filesystem └── S3: Upload to bucket β”‚ β–Ό 4. Source metadata saved to MongoDB { sourceId, workspaceId, fileName, storageKey, storageUrl, type } β”‚ β–Ό 5. Node.js calls FastAPI for processing POST /api/process Body: { storageKey, sourceId, type } β”‚ β–Ό 6. FastAPI DocumentProcessor β”œβ”€β”€ Fetches file from storage β”œβ”€β”€ Extracts text (PyMuPDF for PDFs) β”œβ”€β”€ Semantic chunking at paragraph/sentence boundaries └── Generates embeddings (Sentence Transformers) β”‚ β–Ό 7. ChromaDB stores embeddings { doc_id, chunks: [...], embeddings: [...] } β”‚ β–Ό 8. Response returned to frontend { success: true, chunks: 42, docId: "xxx" }

Step 3: RAG Chat Flow

1. User asks: "What is the main topic?" β”‚ β–Ό 2. Frontend sends via WebSocket WS: /ws/rag_chat Message: { query, document_ids: [...], chat_history: [...] } β”‚ β–Ό 3. RAGAgent.retrieve_context() β”œβ”€β”€ Query embedding generated β”œβ”€β”€ ChromaDB similarity search (TOP_K = 8) └── Returns relevant text chunks β”‚ β–Ό 4. RAGAgent.build_prompt() β”œβ”€β”€ System prompt (AI tutor personality) β”œβ”€β”€ Conversation memory (last 6 messages) β”œβ”€β”€ Retrieved context with [Source N] labels └── User question β”‚ β–Ό 5. LLM Provider generates response β”œβ”€β”€ Factory selects provider (Gemini/OpenAI/DeepInfra) β”œβ”€β”€ Streaming tokens via WebSocket └── Citations linked to sources β”‚ β–Ό 6. Frontend displays with Markdown rendering

Step 4: Content Generation Flow

1. User clicks "Generate Study Materials" β”‚ β–Ό 2. FastAPI receives with progress streaming (SSE) β”‚ β–Ό 3. DeepContentGenerator Pipeline: Stage 1 (5%): Content Analysis β”œβ”€β”€ LLM extracts: main_theme, key_concepts, entities └── Complexity estimation β”‚ β–Ό Stage 2 (10%): Outline Generation └── Hierarchical structure with titles/subtitles β”‚ β–Ό Stage 3 (10-50%): Section Deep Dives β”œβ”€β”€ Detailed explanations per section └── Examples and applications β”‚ β–Ό Stage 4 (50-70%): Visual Generation β”œβ”€β”€ Mermaid concept map └── Process flowcharts β”‚ β–Ό Stage 5 (70-85%): Flashcards └── 12+ cards with term/definition pairs β”‚ β–Ό Stage 6 (85-95%): Quizzes β”œβ”€β”€ 5+ MCQ questions └── Distractor explanations β”‚ β–Ό Stage 7 (95-100%): Final Assembly

3.3 LLM Provider (Simple .env Switch)

Switching LLM providers requires only updating 2 environment variables in ai-engine/.env:

# Just change these two lines to switch providers:
LLM_PROVIDER=gemini          # Options: gemini, openai, deepinfra, ollama, bedrock
GOOGLE_API_KEY=your-api-key  # Add the corresponding API key

Available Providers:

Provider LLM_PROVIDER Value API Key Variable Example Model
Google Gemini gemini GOOGLE_API_KEY gemini-2.0-flash
OpenAI openai OPENAI_API_KEY gpt-4-turbo
DeepInfra deepinfra DEEPINFRA_API_KEY Various OSS models
Ollama (Local) ollama None (uses OLLAMA_HOST) llama2, mistral
AWS Bedrock bedrock AWS credentials Claude, Titan

Example Configurations:

# Use Google Gemini (default)
LLM_PROVIDER=gemini
GOOGLE_API_KEY=AIzaSy...

# Use OpenAI
LLM_PROVIDER=openai
OPENAI_API_KEY=sk-...

# Use local Ollama (no API key needed)
LLM_PROVIDER=ollama
OLLAMA_HOST=http://localhost:11434
Note: No code changes required. Just update .env and restart the backend.

3.4 Vector Store (ChromaDB)

Configuration:

CHROMA_PERSIST_DIR=./chroma_db
EMBEDDING_MODEL=all-MiniLM-L6-v2
CHUNK_SIZE=500
CHUNK_OVERLAP=100

Key Operations:

Operation Description
add_documents Store document chunks with embeddings
query Semantic similarity search with doc_id filtering
delete_document Remove all chunks for a document
embed_text Generate embedding for single text

Semantic Chunking Strategy:

  1. Split on paragraph boundaries (\n\n)
  2. Further split large paragraphs at sentence boundaries
  3. Merge tiny chunks (< 100 chars) with neighbors
  4. Include parent window indices for context expansion

4. API Endpoints Summary

Node.js Backend (localhost:4000)

Endpoint Method Purpose
/api/auth/register POST User registration
/api/auth/login POST JWT authentication
/api/auth/google POST Google OAuth login
/api/workspaces CRUD Workspace management
/api/workspaces/:id/sources CRUD Source (document) management
/api/chat/sessions CRUD Chat session storage
/health GET Health check

FastAPI Backend (localhost:8000)

Endpoint Method Purpose
/api/process POST Document processing + embedding
/api/generate POST Deep content generation (SSE)
/api/chat POST RAG query (non-streaming)
/ws/rag_chat WebSocket Real-time RAG chat
/api/scrape POST URL content scraping
/docs GET Swagger documentation
/health GET Health check

5. Environment Variables Reference

Node.js Backend (.env)

# Server
NODE_ENV=development
PORT=4000
CORS_ORIGINS=http://localhost:5173

# Database
MONGODB_URI=mongodb://localhost:27017/rag_app

# JWT (shared with FastAPI)
JWT_SECRET=your-super-secret-key
JWT_EXPIRES_IN=7d

# Storage
STORAGE_PROVIDER=local
LOCAL_STORAGE_PATH=../application-data
AWS_S3_BUCKET=your-bucket
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=xxx
AWS_SECRET_ACCESS_KEY=xxx

# OAuth
GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com

FastAPI Backend (.env)

# LLM
LLM_PROVIDER=gemini
GOOGLE_API_KEY=your-api-key
DEEPINFRA_API_KEY=xxx
OPENAI_API_KEY=xxx

# Vector Store
CHROMA_PERSIST_DIR=./chroma_db
EMBEDDING_MODEL=all-MiniLM-L6-v2

# Storage (same as Node.js)
STORAGE_PROVIDER=local
LOCAL_STORAGE_PATH=../application-data

# JWT (shared)
JWT_SECRET=your-super-secret-key

6. Advanced Retrieval Engine

The RAG system uses a sophisticated multi-stage retrieval pipeline for significantly improved accuracy:

6.1 Retrieval Architecture

User Query: "What are the key benefits?" β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Step 1: HyDE Query Expansion β”‚ β”‚ "Hypothetical answer that β”‚ β”‚ would contain benefits..." β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Vector Search β”‚ β”‚ BM25 Keyword β”‚ β”‚ (Semantic) β”‚ β”‚ (Exact Match) β”‚ β”‚ Top 15 resultsβ”‚ β”‚ Top 15 resultsβ”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Step 4: Reciprocal Rank β”‚ β”‚ Fusion (RRF) β”‚ β”‚ Combines both result sets β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Step 5: Cross-Encoder β”‚ β”‚ Re-Ranking β”‚ β”‚ ms-marco-MiniLM-L-6-v2 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό Top 5 Final Results

6.2 Retrieval Techniques

Technique Description Why It Helps
HyDE LLM generates hypothetical answer, used for vector search Better semantic alignment with document chunks
Hybrid Search Combines vector similarity + BM25 keyword search Catches both semantic and exact matches
RRF Fusion Reciprocal Rank Fusion combines multiple result lists Leverages strengths of different search methods
Cross-Encoder Re-Ranking Neural model scores query-document pairs More accurate relevance scoring than bi-encoder

6.3 Re-Ranking Model

The system uses cross-encoder/ms-marco-MiniLM-L-6-v2:


7. Error Handling

7.1 FastAPI Error Handling

# Graceful LLM failure handling
try:
    response = await self.llm.generate(messages)
    return response.content
except Exception as e:
    print(f"[RAGAgent] Error: {e}")
    return "I encountered an error. Please try again."

7.2 JSON Repair for LLM Outputs

The DeepContentGenerator includes robust JSON parsing:

7.3 Storage Fallbacks

// Node.js: Graceful storage errors
async getFileBuffer(key: string): Promise<Buffer> {
  try {
    return await readFile(this.getFullPath(key));
  } catch (error) {
    logger.error(`Failed to read file: ${key}`, error);
    throw new NotFoundError(`File not found: ${key}`);
  }
}

8. Email Service (Nodemailer)

The application uses Nodemailer for sending OTP verification emails during user registration.

8.1 Environment Variables

Add these to your node-backend/.env file:

# Email Configuration (Zoho Mail - Default)
EMAIL_USER=your-email@zoho.in
EMAIL_PASSWORD=your-zoho-app-password

8.2 SMTP Configuration

Default Setup (Zoho Mail India):

// services/emailService.ts
const transporter = nodemailer.createTransport({
  host: "smtp.zoho.in",       // Zoho India SMTP server
  port: 465,                   // SSL port
  secure: true,                // Use TLS encryption
  auth: {
    user: config.emailUser,    // EMAIL_USER from .env
    pass: config.emailPassword // EMAIL_PASSWORD from .env
  },
});
Setting Value
Host smtp.zoho.in
Port 465 (SSL)
Secure true
Authentication Email + App Password

8.3 Alternative Email Providers

To switch providers, modify emailService.ts:

Gmail:

const transporter = nodemailer.createTransport({
  host: "smtp.gmail.com",
  port: 587,
  secure: false,
  auth: {
    user: config.emailUser,
    pass: config.emailPassword, // Use App Password (not regular password)
  },
});

SendGrid (Recommended for Production):

const transporter = nodemailer.createTransport({
  host: "smtp.sendgrid.net",
  port: 587,
  auth: {
    user: "apikey",
    pass: process.env.SENDGRID_API_KEY,
  },
});

8.4 Email Features

Feature Implementation
OTP Generation 6-digit random code via Math.random()
HTML Templates Professional styled emails with gradient design
Plain Text Fallback For email clients without HTML support
Custom Headers X-Priority and X-Mailer for deliverability
Branding "Cognify" branded email templates

8.5 Getting Zoho Credentials

  1. Create a Zoho Mail account at mail.zoho.in
  2. Go to Settings β†’ Security β†’ App Passwords
  3. Click Generate New Password
  4. Select "Other Apps" as the application
  5. Copy the generated 16-character password
  6. Use your Zoho email as EMAIL_USER
  7. Use the App Password as EMAIL_PASSWORD

8.6 Email Template Preview

The OTP email includes:

8.7 Troubleshooting

Issue Solution
Authentication failed Use App Password, not regular password
Connection timeout Check firewall allows port 465/587
Email not received Check spam folder, verify sender domain
Rate limiting Use production provider (SendGrid/SES)