Vendo AI Platform - Technical Documentation
Comprehensive technical documentation for developers maintaining and extending the Vendo AI platform.
About This Documentation
This documentation covers the complete technical implementation of Vendo AI, including architecture, deployment procedures, API references, and detailed explanations of all core systems. It is designed for developers who will maintain, extend, or integrate with the platform.
Platform Overview
Vendo AI is a comprehensive sales automation platform that enables businesses to create AI-powered sales agents. The platform leverages OpenAI's GPT-4o model to generate company descriptions, sales scripts, and intelligent conversation flows.
Key Features
- AI Sales Agent Creation: Create custom AI agents with company-specific information and sales scripts
- Automated Company Description Generation: Web scraping and AI-powered analysis to generate comprehensive company profiles
- 7-Stage Sales Script Generation: Structured sales conversation scripts with multiple conversation paths
- User Authentication: Email/password and Google OAuth integration
- User Management: Complete user profile and session management
- Email Service: Automated email verification and password reset functionality
Technology Stack
Frontend
- React 18 with TypeScript
- Vite build tool
- React Router v6
- shadcn/ui component library
- Tailwind CSS
- Lucide React icons
Backend
- NestJS framework
- TypeORM
- PostgreSQL database
- JWT authentication
- Passport.js (Google OAuth)
- Nodemailer
AI & External Services
- OpenAI GPT-4o API
- Axios HTTP client
- Cheerio web scraper
Infrastructure
- Nginx reverse proxy
- PM2 process manager
- Let's Encrypt SSL certificates
- Linux server (Ubuntu/Debian)
System Architecture
High-Level Architecture
The Vendo AI platform follows a modern three-tier architecture:
Presentation Layer
React SPA served via Nginx
Port: 443 (HTTPS)
Application Layer
NestJS REST API
Port: 3001 (Internal)
Data Layer
PostgreSQL Database
Port: 5432 (Internal)
Directory Structure
/srv/
├── vendo-ai/ # Main application directory
│ ├── frontend/ # React frontend application
│ │ ├── src/
│ │ │ ├── components/ # React components
│ │ │ ├── services/ # API service layer
│ │ │ ├── config/ # Configuration files
│ │ │ └── App.tsx # Main app component
│ │ ├── dist/ # Built frontend (production)
│ │ └── package.json
│ └── backend/ # NestJS backend (if separate)
│
├── vendo-docs/ # Documentation site (this site)
│
└── /root/services/
└── vendo-auth/ # Authentication service
├── src/
│ ├── auth/ # Authentication module
│ ├── users/ # User management
│ ├── agents/ # AI agents module
│ │ ├── ai-services/ # Modular AI services
│ │ │ ├── base-ai.service.ts
│ │ │ ├── web-scraper.service.ts
│ │ │ ├── company-description.service.ts
│ │ │ └── sales-script.service.ts
│ │ ├── agents.controller.ts
│ │ ├── agents.service.ts
│ │ └── agent.entity.ts
│ └── main.ts
└── package.json
Modular AI Architecture
The AI engine follows a modular architecture where each AI task is isolated in its own service. This prevents prompt conflicts and allows independent editing:
Abstract base class with common OpenAI functionality
Shared web scraping for website content extraction
Generates company profiles from website content
Generates 7-stage sales conversation scripts
Request Flow
- Client Request: User action in React frontend
- API Service: Frontend service layer calls API endpoint
- Authentication: JWT token verified by guards
- Controller: NestJS controller receives request
- Service Layer: Business logic executed in services
- AI Processing: OpenAI API called if needed
- Database: Data persisted via TypeORM
- Response: JSON response sent back to client
Deployment Guide
Server Requirements
- Ubuntu 20.04+ or Debian 10+
- Node.js 18+ and npm
- PostgreSQL 13+
- Nginx
- PM2 process manager
- Minimum 2GB RAM, 2 CPU cores
Initial Server Setup
# Update system
sudo apt update && sudo apt upgrade -y
# Install Node.js 18
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs
# Install PostgreSQL
sudo apt install -y postgresql postgresql-contrib
# Install Nginx
sudo apt install -y nginx
# Install PM2 globally
sudo npm install -g pm2
Database Setup
# Switch to postgres user
sudo -u postgres psql
-- Create database and user
CREATE DATABASE vendo_ai;
CREATE USER vendo_admin WITH ENCRYPTED PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE vendo_ai TO vendo_admin;
-- Exit psql
\q
Backend Deployment
# Navigate to backend directory
cd /root/services/vendo-auth
# Install dependencies
npm install
# Create .env file
cat > .env << EOF
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=vendo_admin
DATABASE_PASSWORD=your_secure_password
DATABASE_NAME=vendo_ai
JWT_SECRET=your_jwt_secret_key
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
OPENAI_API_KEY=your_openai_api_key
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=your_email@gmail.com
EMAIL_PASSWORD=your_email_app_password
FRONTEND_URL=https://vendo-ai.com
EOF
# Build the application
npm run build
# Start with PM2
pm2 start dist/main.js --name vendo-auth
# Save PM2 configuration
pm2 save
pm2 startup
Frontend Deployment
# Navigate to frontend directory
cd /srv/vendo-ai/frontend
# Install dependencies
npm install
# Build production bundle
npm run build
# The built files will be in /srv/vendo-ai/frontend/dist
Nginx Configuration
Create Nginx configuration file:
# /etc/nginx/sites-available/vendo-ai.com
server {
listen 80;
server_name vendo-ai.com www.vendo-ai.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name vendo-ai.com www.vendo-ai.com;
ssl_certificate /etc/letsencrypt/live/vendo-ai.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vendo-ai.com/privkey.pem;
# Frontend (React SPA)
root /srv/vendo-ai/frontend/dist;
index index.html;
# API proxy
location /api/ {
proxy_pass http://localhost:3001/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# React Router fallback
location / {
try_files $uri $uri/ /index.html;
}
}
SSL Certificate Setup
# Install Certbot
sudo apt install -y certbot python3-certbot-nginx
# Obtain SSL certificate
sudo certbot --nginx -d vendo-ai.com -d www.vendo-ai.com
# Test auto-renewal
sudo certbot renew --dry-run
Enable and Start Services
# Enable Nginx site
sudo ln -s /etc/nginx/sites-available/vendo-ai.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
# Verify PM2 is running
pm2 status
pm2 logs vendo-auth
Updating the Application
# Update backend
cd /root/services/vendo-auth
git pull # or upload new files
npm install
npm run build
pm2 restart vendo-auth
# Update frontend
cd /srv/vendo-ai/frontend
git pull # or upload new files
npm install
npm run build
# No restart needed - Nginx serves static files
Frontend Structure
Application Entry Points
The React application is structured around key entry points:
Main Application (App.tsx)
// /srv/vendo-ai/frontend/src/App.tsx
- Manages authentication state
- Implements dual token storage (localStorage + sessionStorage)
- Provides routing with React Router
- Protects routes with authentication checks
Key Components
Authentication Components
WelcomePage.tsx- Landing page with auth optionsAuthPage.tsx- Login/register formsEmailVerificationPage.tsx- Email verification handlerGoogleAuthCallback.tsx- Google OAuth callbackResetPasswordPage.tsx- Password reset form
Main Application Components
DashboardPage.tsx- Main dashboard with quick actionsSalesAgentCreator.tsx- Multi-step agent creation wizardAgentsListPage.tsx- List of user's created agentsProfilePage.tsx- User profile and settings
Service Layer
The frontend uses service classes to communicate with the backend API:
AuthService
// /srv/vendo-ai/frontend/src/services/auth.service.ts
class AuthService {
// Dual storage strategy for token persistence
static async login(email, password)
static async register(email, password, name)
static async loginWithGoogle()
static async verifyEmail(token)
static async forgotPassword(email)
static async resetPassword(token, newPassword)
static logout()
static getCurrentUser()
}
AgentService
// /srv/vendo-ai/frontend/src/services/agent.service.ts
class AgentService {
static async create(agentData)
static async getAll()
static async getById(id)
static async update(id, agentData)
static async delete(id)
static async generateDescription(agentId, companyName, website)
static async generateScript(agentId, scriptData)
}
State Management
The application uses React hooks for state management:
- useState: Local component state
- useEffect: Side effects and data fetching
- useNavigate: Programmatic navigation
- Custom events: Cross-component communication (auth-change)
Token Persistence Strategy
To prevent token loss during cache resets, the application implements a dual storage approach:
// Save to both storages
localStorage.setItem('access_token', token);
sessionStorage.setItem('access_token', token);
// Restore from sessionStorage if localStorage is cleared
if (sessionStorage.getItem('access_token') && !localStorage.getItem('access_token')) {
localStorage.setItem('access_token', sessionStorage.getItem('access_token'));
}
// Check both storages when retrieving
const token = localStorage.getItem('access_token') || sessionStorage.getItem('access_token');
API Configuration
// /srv/vendo-ai/frontend/src/config/api.ts
export const API_CONFIG = {
baseUrl: import.meta.env.VITE_API_URL || 'https://vendo-ai.com/api',
endpoints: {
auth: {
login: '/auth/login',
register: '/auth/register',
googleAuth: '/auth/google',
verifyEmail: (token) => `/auth/verify-email/${token}`,
// ...
},
agents: {
create: '/agents',
getAll: '/agents',
generateDescription: (id) => `/agents/${id}/generate-description`,
// ...
}
}
};
// Centralized request handler with authentication
export async function apiRequest(url, options) {
const token = localStorage.getItem('access_token') ||
sessionStorage.getItem('access_token');
// Add Authorization header
// Handle empty responses (DELETE operations)
// Parse JSON only if response has content
}
UI Component Library
The application uses shadcn/ui components built on Radix UI:
- Button: Primary actions and navigation
- Input: Form fields with validation
- Card: Content containers
- Dialog/AlertDialog: Modals and confirmations
- Textarea: Multi-line text input
- Select: Dropdown selections
- RadioGroup: Single choice selections
- Toaster: Toast notifications
Build Configuration
// vite.config.ts
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
});
Backend Services
NestJS Application Structure
The backend follows NestJS modular architecture with clear separation of concerns:
Main Modules
Authentication Module
/root/services/vendo-auth/src/auth/
- auth.controller.ts: Authentication endpoints
- auth.service.ts: Authentication business logic
- jwt.strategy.ts: JWT validation strategy
- google.strategy.ts: Google OAuth strategy
- jwt-auth.guard.ts: Route protection guard
Users Module
/root/services/vendo-auth/src/users/
- user.entity.ts: User database model
- users.service.ts: User management operations
- users.controller.ts: User-related endpoints
Agents Module
/root/services/vendo-auth/src/agents/
- agent.entity.ts: Agent database model
- agents.service.ts: Agent CRUD operations
- agents.controller.ts: Agent endpoints
- agents.module.ts: Module configuration
Database Models
User Entity
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column({ nullable: true })
password: string;
@Column()
name: string;
@Column({ default: false })
emailVerified: boolean;
@Column({ nullable: true })
googleId: string;
@Column({ nullable: true })
resetPasswordToken: string;
@Column({ nullable: true })
resetPasswordExpires: Date;
@OneToMany(() => Agent, agent => agent.user)
agents: Agent[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
Agent Entity
@Entity('agents')
export class Agent {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column()
companyName: string;
@Column()
companyWebsite: string;
@Column('text')
productDescription: string;
@Column('text', { nullable: true })
documents: string;
@Column({
type: 'enum',
enum: ScriptStyle,
default: ScriptStyle.PROFESSIONAL_FORMAL
})
scriptStyle: ScriptStyle;
@Column('jsonb', { nullable: true })
scriptSections: any;
@ManyToOne(() => User, user => user.agents, { onDelete: 'CASCADE' })
user: User;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
Dependency Injection
NestJS uses dependency injection to manage service instances:
@Injectable()
export class AgentsService {
constructor(
@InjectRepository(Agent)
private agentsRepository: Repository,
private companyDescriptionService: CompanyDescriptionService,
private salesScriptService: SalesScriptService,
) {}
}
Request Guards
Routes are protected using JWT authentication guards:
@Controller('agents')
@UseGuards(JwtAuthGuard)
export class AgentsController {
@Get()
async findAll(@Request() req) {
return this.agentsService.findAllByUser(req.user.id);
}
}
Error Handling
// Global exception filter
try {
const agent = await this.agentsRepository.findOne({ where: { id, user: { id: userId } } });
if (!agent) {
throw new NotFoundException('Agent not found');
}
return agent;
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new InternalServerErrorException('Failed to fetch agent');
}
TypeORM Configuration
// app.module.ts
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT),
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
entities: [User, Agent],
synchronize: true, // Set to false in production
logging: false,
})
AI Engine
Modular Architecture Overview
The AI engine is built with a modular architecture where each AI task is isolated in its own service. This prevents conflicts between different prompts and allows independent editing without affecting other AI functionalities.
Why Modular Architecture?
- Separation of concerns: Each service handles one specific AI task
- Easy testing: Services can be tested independently
- Code reusability: Base classes and utilities are shared
- Change isolation: Modifying one prompt doesn't affect others
- Scalability: Easy to add new AI services
- Maintainability: Each file contains only one type of generation
Base AI Service
/root/services/vendo-auth/src/agents/ai-services/base-ai.service.ts
Abstract base class providing common OpenAI functionality for all AI services.
export abstract class BaseAIService {
protected readonly apiKey: string | undefined;
protected readonly apiUrl = 'https://api.openai.com/v1/chat/completions';
constructor() {
this.apiKey = process.env.OPENAI_API_KEY;
}
protected validateApiKey(): void {
if (!this.apiKey) {
throw new Error('OpenAI API key is not configured');
}
}
protected async callOpenAI(params: {
systemPrompt: string;
userPrompt: string;
model?: string;
temperature?: number;
maxTokens?: number;
}): Promise {
this.validateApiKey();
const response = await axios.post(
this.apiUrl,
{
model: params.model || 'gpt-4o',
messages: [
{ role: 'system', content: params.systemPrompt },
{ role: 'user', content: params.userPrompt }
],
temperature: params.temperature || 0.7,
max_tokens: params.maxTokens || 4000,
},
{
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
}
);
return response.data.choices[0].message.content;
}
}
Web Scraper Service
/root/services/vendo-auth/src/agents/ai-services/web-scraper.service.ts
Shared service for extracting content from websites used by multiple AI services.
Features:
- Automatic protocol addition (http/https)
- User-Agent header to bypass basic bot detection
- Removal of non-content elements (scripts, styles, nav, footer)
- Extraction of title, meta description, headings (H1, H2, H3)
- Main content extraction (limited to 15,000 characters)
- Support for scraping multiple pages
@Injectable()
export class WebScraperService {
async scrapeWebsite(url: string): Promise {
// Add protocol if missing
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
const response = await axios.get(url, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0...',
},
});
const $ = cheerio.load(response.data);
// Remove non-content elements
$('script, style, nav, footer, iframe, noscript').remove();
// Extract structured content
const title = $('title').text().trim();
const metaDescription = $('meta[name="description"]').attr('content');
const h1Texts = $('h1').map((_, el) => $(el).text().trim()).get().join(' | ');
const bodyText = $('body').text().replace(/\\s+/g, ' ').trim().substring(0, 15000);
return `Page Title: ${title}\\n\\nMeta Description: ${metaDescription}\\n\\nMain Headings: ${h1Texts}\\n\\nContent:\\n${bodyText}`;
}
async scrapeMultiplePages(urls: string[]): Promise
Company Description Service
/root/services/vendo-auth/src/agents/ai-services/company-description.service.ts
Generates comprehensive company profiles by analyzing website content.
Methods:
- generateFromWebsite(): Scrapes website and generates description
- generateFromText(): Generates description from provided text
What it generates:
- Company Overview
- Products/Services
- Pricing Information
- Special Offers
- Company Values/Mission
- Contact Information
- Key Features/Benefits
- Target Audience
- Company Background
- Social Proof
Configuration:
- Model:
gpt-4o - Temperature:
0.7 - Max tokens:
4000
Sales Script Service
/root/services/vendo-auth/src/agents/ai-services/sales-script.service.ts
Generates structured 7-stage sales conversation scripts designed for 20-30 minute calls.
7-Stage Structure:
- STAGE 1 — WELCOME: Greeting and establishing rapport
- STAGE 2 — SMALL TALK: Building trust and connection
- STAGE 3 — AGENDA: Setting meeting expectations
- STAGE 4 — QUALIFICATION: Discovery questions and needs analysis
- STAGE 5 — PRESENTATION: Value proposition and solution details
- STAGE 6 — PRICING: Pricing discussion and objection handling
- STAGE 7 — CLOSING: Closing techniques and next steps
Script Styles:
- PROFESSIONAL_FORMAL: Professional and formal tone
- FRIENDLY_CASUAL: Friendly and approachable tone
- TECHNICAL_EXPERT: Technical and detailed tone
- CONSULTATIVE: Advisory and solution-focused tone
Configuration:
- Model:
gpt-4o - Temperature:
0.7 - Max tokens:
16000(for comprehensive scripts)
Response Parsing:
The service parses OpenAI's response into structured sections with icons:
interface ScriptSection {
id: string; // 'stage1', 'stage2', etc.
title: string; // 'STAGE 1 — WELCOME'
content: string; // The actual script content
icon: string; // Icon name for UI display
}
Adding New AI Services
To add a new AI service, follow these steps:
Step 1: Create the Service
// ai-services/my-new-ai.service.ts
import { Injectable } from '@nestjs/common';
import { BaseAIService } from './base-ai.service';
@Injectable()
export class MyNewAIService extends BaseAIService {
async generateSomething(params: { input: string }): Promise {
this.validateApiKey();
const systemPrompt = 'You are an expert in...';
const userPrompt = `Generate something based on: ${params.input}`;
return this.callOpenAI({
systemPrompt,
userPrompt,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 4000,
});
}
}
Step 2: Register in Module
// agents.module.ts
import { MyNewAIService } from './ai-services/my-new-ai.service';
@Module({
providers: [
// ...existing services
MyNewAIService,
],
exports: [
// ...existing services
MyNewAIService,
],
})
Step 3: Use in Controller
// agents.controller.ts
constructor(
private readonly myNewAIService: MyNewAIService,
) {}
@Post('generate-something')
async generateSomething(@Body() body: any) {
const result = await this.myNewAIService.generateSomething({
input: body.input,
});
return { result };
}
Best Practices
- One service = one AI task
- Inherit from BaseAIService for common functionality
- Use WebScraperService for website content extraction
- Document prompts in method comments
- Version control prompt changes in git commit messages
- Test prompts thoroughly before deployment
- Keep prompt configuration visible and editable
- Handle errors gracefully with user-friendly messages
Authentication System
Authentication Methods
The platform supports two authentication methods:
- Email/Password: Traditional credentials with email verification
- Google OAuth 2.0: Single sign-on via Google
JWT Token Strategy
The system uses JWT (JSON Web Tokens) for stateless authentication:
Token Structure:
{
"sub": "user-uuid",
"email": "user@example.com",
"iat": 1234567890,
"exp": 1234654290 // 24 hours later
}
Token Generation:
// auth.service.ts
async login(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
if (!user || !user.emailVerified) {
throw new UnauthorizedException('Invalid credentials or email not verified');
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials');
}
const payload = { sub: user.id, email: user.email };
return {
access_token: this.jwtService.sign(payload),
refresh_token: this.jwtService.sign(payload, { expiresIn: '7d' }),
user: { id: user.id, email: user.email, name: user.name }
};
}
Email/Password Authentication Flow
Registration:
- User submits email, password, and name
- Backend hashes password with bcrypt (10 salt rounds)
- User record created with
emailVerified: false - Verification email sent with unique token
- User clicks verification link
emailVerifiedset totrue- User can now log in
Login:
- User submits email and password
- Backend finds user by email
- Verifies email is confirmed
- Compares password hash with bcrypt
- Generates JWT access and refresh tokens
- Returns tokens and user data
- Frontend stores tokens in localStorage and sessionStorage
Password Reset:
- User requests password reset with email
- Backend generates unique reset token (UUID)
- Token stored in database with 1-hour expiration
- Reset email sent with link containing token
- User clicks link and enters new password
- Backend validates token and expiration
- Password hashed and updated
- Reset token cleared from database
Google OAuth Flow
- User clicks "Continue with Google" button
- Frontend redirects to
/api/auth/google - Backend initiates OAuth flow with Google
- User authenticates with Google
- Google redirects to
/api/auth/google/callback - Backend receives user profile from Google
- Check if user exists by
googleIdoremail - Create new user or update existing one
- Set
emailVerified: true(Google pre-verifies emails) - Generate JWT tokens
- Redirect to frontend with tokens in URL
- Frontend extracts tokens and stores them
Google Strategy Configuration:
// google.strategy.ts
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor() {
super({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'https://vendo-ai.com/api/auth/google/callback',
scope: ['email', 'profile'],
});
}
async validate(accessToken: string, refreshToken: string, profile: any) {
const { id, name, emails, photos } = profile;
const user = {
googleId: id,
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
picture: photos[0].value,
};
return user;
}
}
Route Protection
Protected routes use the JwtAuthGuard:
@Controller('agents')
@UseGuards(JwtAuthGuard)
export class AgentsController {
// All routes require authentication
@Get()
async findAll(@Request() req) {
// req.user contains decoded JWT payload
return this.agentsService.findAllByUser(req.user.id);
}
}
Token Persistence (Dual Storage)
To prevent token loss during cache resets, tokens are stored in both localStorage and sessionStorage:
Token Storage:
// Save to both storages
private static saveTokens(accessToken: string, refreshToken: string, user: any) {
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
localStorage.setItem('user', JSON.stringify(user));
sessionStorage.setItem('access_token', accessToken);
sessionStorage.setItem('refresh_token', refreshToken);
sessionStorage.setItem('user', JSON.stringify(user));
}
Token Retrieval:
// Check both storages
private static getToken(key: string): string | null {
return localStorage.getItem(key) || sessionStorage.getItem(key);
}
Token Restoration:
// App.tsx - Restore from sessionStorage if localStorage is cleared
const [isAuthenticated, setIsAuthenticated] = useState(() => {
const hasToken = !!(localStorage.getItem('access_token') ||
sessionStorage.getItem('access_token'));
if (sessionStorage.getItem('access_token') && !localStorage.getItem('access_token')) {
localStorage.setItem('access_token', sessionStorage.getItem('access_token')!);
localStorage.setItem('refresh_token', sessionStorage.getItem('refresh_token')!);
localStorage.setItem('user', sessionStorage.getItem('user')!);
}
return hasToken;
});
Security Measures
- Password Hashing: bcrypt with 10 salt rounds
- JWT Secret: Strong secret key in environment variables
- Token Expiration: Access tokens expire in 24 hours
- Email Verification: Required before login
- Reset Token Expiration: Password reset tokens expire in 1 hour
- HTTPS Only: All communication over SSL
- CORS: Configured to allow only frontend domain
- Input Validation: DTOs with class-validator decorators
Admin Panel Authentication
The admin panel (https://admin.vendo-ai.com) uses enhanced security with mandatory Two-Factor Authentication (2FA) for all administrators.
Admin Authentication Flow:
- Step 1: Username & Password
POST /auth/login { "username": "admin", "password": "password" } // Response: { "requiresTwoFactor": true } - Step 2: 2FA Code Verification
POST /auth/verify-2fa { "username": "admin", "code": "123456" // 6-digit TOTP code from Google Authenticator } // Response: { "access_token": "eyJhbGciOiJIUzI1NiIs...", "admin": { "id": "uuid", "username": "admin", "email": "admin@example.com", "first_name": "John", "last_name": "Doe" } }
Two-Factor Authentication (2FA):
- Method: Time-based One-Time Password (TOTP) using Google Authenticator
- Library:
speakeasylibrary for TOTP generation and verification - Secret Generation: Unique 32-character base32 secret per administrator
- Code Rotation: 6-digit codes rotate every 30 seconds
- QR Code: Generated on admin creation for easy mobile app setup
Admin Creation with 2FA:
POST /admins
{
"username": "newadmin",
"email": "newadmin@example.com",
"password": "SecurePassword123",
"firstName": "John",
"lastName": "Doe"
}
// Response includes QR code for Google Authenticator:
{
"id": "uuid",
"username": "newadmin",
"qrCode": "data:image/png;base64,...",
"manualEntryKey": "JNGDOXKQGVLG24D5NBL..."
}
Frontend Session Persistence:
The admin panel maintains authentication state across page reloads:
// AuthContext.tsx
useEffect(() => {
const storedToken = localStorage.getItem('adminToken');
const storedAdmin = localStorage.getItem('adminInfo');
if (storedToken && storedAdmin) {
setToken(storedToken);
setAdmin(JSON.parse(storedAdmin));
}
setIsLoading(false);
}, []);
// PrivateRoute with loading state
if (isLoading) {
return Loading...;
}
if (!isAuthenticated) {
return ;
}
Admin Panel Security Features:
- Mandatory 2FA: All administrators must use Google Authenticator
- Separate JWT Secret: Admin panel uses different JWT secret from main app
- Token Expiration: Admin tokens expire in 24 hours
- Protected Routes: All admin routes require authentication
- Persistent Sessions: Tokens stored in localStorage survive page reloads
- Auto-logout on 401: Automatic redirect to login on unauthorized responses
Admin Credentials (Default):
URL: https://admin.vendo-ai.com
Username: admin
Password: [Contact system administrator]
2FA: Required (scan QR code during first login)
Email Service
Overview
The email service handles all outbound emails for the platform, including:
- Email verification for new registrations
- Password reset requests
- Future: Notifications and marketing emails
Email Provider Configuration
The system uses Gmail SMTP with app-specific password:
Environment Variables:
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=your_email@gmail.com
EMAIL_PASSWORD=your_app_specific_password
Setting up Gmail App Password:
- Go to Google Account settings
- Enable 2-Factor Authentication
- Navigate to Security → App passwords
- Generate a new app password for "Mail"
- Copy the 16-character password
- Add to
EMAIL_PASSWORDin .env file
Email Transport Setup
// Nodemailer configuration
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: parseInt(process.env.EMAIL_PORT),
secure: false, // true for 465, false for 587
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD,
},
});
Email Templates
Verification Email:
async sendVerificationEmail(email: string, token: string) {
const verificationUrl = `${process.env.FRONTEND_URL}/verify-email/${token}`;
await this.transporter.sendMail({
from: process.env.EMAIL_USER,
to: email,
subject: 'Verify your Vendo AI account',
html: `
Welcome to Vendo AI!
Thank you for registering. Please verify your email address by clicking the button below:
Verify Email
Or copy and paste this link: ${verificationUrl}
This link will expire in 24 hours.
`,
});
}
Password Reset Email:
async sendPasswordResetEmail(email: string, token: string) {
const resetUrl = `${process.env.FRONTEND_URL}/reset-password/${token}`;
await this.transporter.sendMail({
from: process.env.EMAIL_USER,
to: email,
subject: 'Reset your Vendo AI password',
html: `
Password Reset Request
You requested to reset your password. Click the button below to proceed:
Reset Password
Or copy and paste this link: ${resetUrl}
This link will expire in 1 hour.
If you didn't request this, please ignore this email.
`,
});
}
Email Error Handling
try {
await this.sendVerificationEmail(user.email, token);
} catch (error) {
console.error('Failed to send verification email:', error);
// Log error but don't fail registration
// User can request resend later
}
Alternative Email Providers
The system can be configured to use other SMTP providers:
SendGrid:
EMAIL_HOST=smtp.sendgrid.net
EMAIL_PORT=587
EMAIL_USER=apikey
EMAIL_PASSWORD=your_sendgrid_api_key
AWS SES:
EMAIL_HOST=email-smtp.us-east-1.amazonaws.com
EMAIL_PORT=587
EMAIL_USER=your_ses_smtp_username
EMAIL_PASSWORD=your_ses_smtp_password
Mailgun:
EMAIL_HOST=smtp.mailgun.org
EMAIL_PORT=587
EMAIL_USER=postmaster@your-domain.mailgun.org
EMAIL_PASSWORD=your_mailgun_password
Email Best Practices
- Always use HTML templates with fallback text
- Include both button and plain link for accessibility
- Set appropriate token expiration times
- Log email sending errors for monitoring
- Test emails in different clients (Gmail, Outlook, etc.)
- Use proper sender name and email address
- Include unsubscribe link for marketing emails
- Monitor bounce rates and spam complaints
Testing Email Service
For development, you can use a service like Mailtrap:
EMAIL_HOST=smtp.mailtrap.io
EMAIL_PORT=2525
EMAIL_USER=your_mailtrap_username
EMAIL_PASSWORD=your_mailtrap_password
Database
PostgreSQL Setup
The platform uses PostgreSQL as its primary database.
Database Schema:
Database: vendo_ai
Tables:
- users
- agents
Users Table
Column | Type | Nullable | Default
---------------------|--------------|----------|----------
id | uuid | NO | uuid_generate_v4()
email | varchar(255) | NO |
password | varchar(255) | YES | NULL
name | varchar(255) | NO |
emailVerified | boolean | NO | false
googleId | varchar(255) | YES | NULL
resetPasswordToken | varchar(255) | YES | NULL
resetPasswordExpires | timestamp | YES | NULL
createdAt | timestamp | NO | now()
updatedAt | timestamp | NO | now()
Indexes:
- PRIMARY KEY (id)
- UNIQUE (email)
- INDEX (googleId)
Agents Table
Column | Type | Nullable | Default
-------------------|--------------|----------|------------------------
id | uuid | NO | uuid_generate_v4()
name | varchar(255) | NO |
companyName | varchar(255) | NO |
companyWebsite | varchar(255) | NO |
productDescription | text | NO |
documents | text | YES | NULL
scriptStyle | enum | NO | 'PROFESSIONAL_FORMAL'
scriptSections | jsonb | YES | NULL
userId | uuid | NO |
createdAt | timestamp | NO | now()
updatedAt | timestamp | NO | now()
Indexes:
- PRIMARY KEY (id)
- FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
- INDEX (userId)
Relationships
users 1 ──── N agents
(one user can have many agents)
Cascade Delete: When a user is deleted, all their agents are automatically deleted.
Database Queries
Create User:
INSERT INTO users (id, email, password, name, emailVerified)
VALUES (uuid_generate_v4(), 'user@example.com', '$2b$10$...', 'John Doe', false);
Find User by Email:
SELECT * FROM users WHERE email = 'user@example.com';
Update Email Verification:
UPDATE users SET "emailVerified" = true WHERE id = 'user-uuid';
Create Agent:
INSERT INTO agents (id, name, "companyName", "companyWebsite",
"productDescription", "scriptStyle", "userId")
VALUES (uuid_generate_v4(), 'Sales Agent', 'Acme Corp',
'https://acme.com', 'Description...', 'PROFESSIONAL_FORMAL', 'user-uuid');
Find All Agents for User:
SELECT * FROM agents WHERE "userId" = 'user-uuid' ORDER BY "createdAt" DESC;
Delete Agent:
DELETE FROM agents WHERE id = 'agent-uuid' AND "userId" = 'user-uuid';
Backup and Restore
Create Backup:
# Full database backup
pg_dump -U vendo_admin -d vendo_ai -F c -f vendo_ai_backup_$(date +%Y%m%d).dump
# Backup with compressed format
pg_dump -U vendo_admin -d vendo_ai -F c -Z 9 -f vendo_ai_backup.dump
Restore from Backup:
# Restore database
pg_restore -U vendo_admin -d vendo_ai -c vendo_ai_backup.dump
# Or with verbose output
pg_restore -U vendo_admin -d vendo_ai -v vendo_ai_backup.dump
Automated Backups:
# Create backup script
cat > /usr/local/bin/backup-vendo-db.sh << 'EOF'
#!/bin/bash
BACKUP_DIR="/var/backups/vendo-ai"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
pg_dump -U vendo_admin -d vendo_ai -F c -f $BACKUP_DIR/vendo_ai_$DATE.dump
# Keep only last 7 days
find $BACKUP_DIR -name "vendo_ai_*.dump" -mtime +7 -delete
EOF
chmod +x /usr/local/bin/backup-vendo-db.sh
# Add to crontab (daily at 2 AM)
crontab -e
0 2 * * * /usr/local/bin/backup-vendo-db.sh
Database Maintenance
Vacuum and Analyze:
-- Reclaim storage and update statistics
VACUUM ANALYZE users;
VACUUM ANALYZE agents;
Check Database Size:
SELECT pg_size_pretty(pg_database_size('vendo_ai'));
Check Table Sizes:
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
Connection Pooling
TypeORM handles connection pooling automatically:
TypeOrmModule.forRoot({
type: 'postgres',
// ... connection details
poolSize: 10, // Maximum number of connections in pool
extra: {
max: 10,
min: 2,
idleTimeoutMillis: 30000,
},
})
API Reference
Complete REST API documentation for integrating with the Vendo AI platform.
Base URL
https://vendo-ai.com/api
All API requests must be made over HTTPS. HTTP requests will be automatically redirected to HTTPS.
Authentication
Most endpoints require authentication via JWT token in the Authorization header:
Authorization: Bearer {access_token}
POST /auth/register
Register a new user account.
{
"email": "user@example.com",
"password": "SecurePassword123!",
"name": "John Doe"
}
Response (201 Created):
{
"message": "User registered successfully. Please check your email to verify your account."
}
Errors:
- 400: Invalid email format or weak password
- 409: Email already registered
POST /auth/login
Login with email and password.
{
"email": "user@example.com",
"password": "SecurePassword123!"
}
Response (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "John Doe"
}
}
Errors:
- 401: Invalid credentials or email not verified
GET /auth/google
Initiate Google OAuth flow. Redirects to Google login page.
Redirect to Google OAuth consent screen
GET /auth/google/callback
Google OAuth callback. Handled automatically by Passport.
Redirect to frontend with tokens in URL parameters
GET /auth/verify-email/:token
Verify user email address.
token- Verification token from email
{
"message": "Email verified successfully"
}
Errors:
- 400: Invalid or expired token
POST /auth/forgot-password
Request password reset email.
{
"email": "user@example.com"
}
Response (200 OK):
{
"message": "Password reset email sent"
}
POST /auth/reset-password/:token
Reset password with token from email.
token- Reset token from email
{
"newPassword": "NewSecurePassword123!"
}
Response (200 OK):
{
"message": "Password reset successfully"
}
Errors:
- 400: Invalid or expired token
- 400: Weak password
Users
GET /users/profile
Get current user profile. Requires authentication.
Authorization: Bearer {access_token}
Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "John Doe",
"emailVerified": true,
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
Errors:
- 401: Unauthorized (invalid or missing token)
PATCH /users/profile
Update current user profile. Requires authentication.
Authorization: Bearer {access_token}
Request Body:
{
"name": "John Smith"
}
Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "John Smith",
"emailVerified": true,
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T12:45:00.000Z"
}
Errors:
- 401: Unauthorized
Agents
POST /agents
Create a new AI sales agent. Requires authentication.
Authorization: Bearer {access_token}
Request Body:
{
"name": "Sales Agent 1",
"companyName": "Acme Corp",
"companyWebsite": "https://acme.com",
"productDescription": "Comprehensive company description...",
"documents": "Additional documents or context...",
"scriptStyle": "PROFESSIONAL_FORMAL"
}
Script Styles:
PROFESSIONAL_FORMALFRIENDLY_CASUALTECHNICAL_EXPERTCONSULTATIVE
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Sales Agent 1",
"companyName": "Acme Corp",
"companyWebsite": "https://acme.com",
"productDescription": "Comprehensive company description...",
"documents": "Additional documents...",
"scriptStyle": "PROFESSIONAL_FORMAL",
"scriptSections": null,
"userId": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2024-01-15T14:20:00.000Z",
"updatedAt": "2024-01-15T14:20:00.000Z"
}
Errors:
- 401: Unauthorized
- 400: Invalid request body
GET /agents
Get all agents for the current user. Requires authentication.
Authorization: Bearer {access_token}
Response (200 OK):
[
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Sales Agent 1",
"companyName": "Acme Corp",
"companyWebsite": "https://acme.com",
"productDescription": "Description...",
"scriptStyle": "PROFESSIONAL_FORMAL",
"createdAt": "2024-01-15T14:20:00.000Z",
"updatedAt": "2024-01-15T14:20:00.000Z"
}
]
Errors:
- 401: Unauthorized
GET /agents/:id
Get a specific agent by ID. Requires authentication.
Authorization: Bearer {access_token}
URL Parameters:
id- Agent UUID
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Sales Agent 1",
"companyName": "Acme Corp",
"companyWebsite": "https://acme.com",
"productDescription": "Description...",
"documents": "Documents...",
"scriptStyle": "PROFESSIONAL_FORMAL",
"scriptSections": [...],
"createdAt": "2024-01-15T14:20:00.000Z",
"updatedAt": "2024-01-15T14:20:00.000Z"
}
Errors:
- 401: Unauthorized
- 404: Agent not found
PATCH /agents/:id
Update an agent. Requires authentication.
Authorization: Bearer {access_token}
URL Parameters:
id- Agent UUID
{
"name": "Updated Agent Name",
"scriptStyle": "FRIENDLY_CASUAL"
}
Response (200 OK):
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Updated Agent Name",
"companyName": "Acme Corp",
"scriptStyle": "FRIENDLY_CASUAL",
...
}
Errors:
- 401: Unauthorized
- 404: Agent not found
DELETE /agents/:id
Delete an agent. Requires authentication.
Authorization: Bearer {access_token}
URL Parameters:
id- Agent UUID
Empty response body
Errors:- 401: Unauthorized
- 404: Agent not found
POST /agents/:id/generate-description
Generate company description from website. Requires authentication.
Authorization: Bearer {access_token}
URL Parameters:
id- Agent UUID
{
"companyName": "Acme Corp",
"companyWebsite": "https://acme.com"
}
Response (200 OK):
{
"description": "**Company Overview**\nAcme Corp is a leading provider...\n\n**Products/Services**\n..."
}
Errors:
- 401: Unauthorized
- 400: Invalid website URL or unable to scrape website
- 500: OpenAI API error
POST /agents/:id/generate-script
Generate 7-stage sales script. Requires authentication.
Authorization: Bearer {access_token}
URL Parameters:
id- Agent UUID
{
"companyName": "Acme Corp",
"companyWebsite": "https://acme.com",
"companyDescription": "Detailed company description...",
"documents": "Optional additional context...",
"scriptStyle": "PROFESSIONAL_FORMAL"
}
Response (200 OK):
{
"sections": [
{
"id": "stage1",
"title": "STAGE 1 — WELCOME",
"content": "Detailed welcome script content...",
"icon": "UserCircle"
},
{
"id": "stage2",
"title": "STAGE 2 — SMALL TALK",
"content": "Small talk script content...",
"icon": "MessageCircle"
},
...
],
"fullText": "Full script text with all stages..."
}
Errors:
- 401: Unauthorized
- 400: Invalid request body
- 500: OpenAI API error
Error Response Format
All errors follow a consistent format:
{
"statusCode": 400,
"message": "Error description",
"error": "Bad Request"
}
Rate Limiting
API endpoints are subject to rate limiting:
- Authentication endpoints: 5 requests per minute per IP
- AI generation endpoints: 10 requests per hour per user
- Other endpoints: 100 requests per minute per user
Integration Examples
JavaScript/Node.js:
const API_URL = 'https://vendo-ai.com/api';
// Login
const loginResponse = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
password: 'password123'
})
});
const { access_token } = await loginResponse.json();
// Create agent
const agentResponse = await fetch(`${API_URL}/agents`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${access_token}`
},
body: JSON.stringify({
name: 'Sales Agent',
companyName: 'Acme Corp',
companyWebsite: 'https://acme.com',
productDescription: 'Description...',
scriptStyle: 'PROFESSIONAL_FORMAL'
})
});
const agent = await agentResponse.json();
Python:
import requests
API_URL = 'https://vendo-ai.com/api'
# Login
login_response = requests.post(f'{API_URL}/auth/login', json={
'email': 'user@example.com',
'password': 'password123'
})
access_token = login_response.json()['access_token']
# Create agent
agent_response = requests.post(f'{API_URL}/agents',
headers={'Authorization': f'Bearer {access_token}'},
json={
'name': 'Sales Agent',
'companyName': 'Acme Corp',
'companyWebsite': 'https://acme.com',
'productDescription': 'Description...',
'scriptStyle': 'PROFESSIONAL_FORMAL'
}
)
agent = agent_response.json()
cURL:
# Login
curl -X POST https://vendo-ai.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password123"}'
# Create agent
curl -X POST https://vendo-ai.com/api/agents \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {access_token}" \
-d '{
"name": "Sales Agent",
"companyName": "Acme Corp",
"companyWebsite": "https://acme.com",
"productDescription": "Description...",
"scriptStyle": "PROFESSIONAL_FORMAL"
}'