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:

BaseAIService

Abstract base class with common OpenAI functionality

WebScraperService

Shared web scraping for website content extraction

CompanyDescriptionService

Generates company profiles from website content

SalesScriptService

Generates 7-stage sales conversation scripts

Request Flow

  1. Client Request: User action in React frontend
  2. API Service: Frontend service layer calls API endpoint
  3. Authentication: JWT token verified by guards
  4. Controller: NestJS controller receives request
  5. Service Layer: Business logic executed in services
  6. AI Processing: OpenAI API called if needed
  7. Database: Data persisted via TypeORM
  8. 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 options
  • AuthPage.tsx - Login/register forms
  • EmailVerificationPage.tsx - Email verification handler
  • GoogleAuthCallback.tsx - Google OAuth callback
  • ResetPasswordPage.tsx - Password reset form

Main Application Components

  • DashboardPage.tsx - Main dashboard with quick actions
  • SalesAgentCreator.tsx - Multi-step agent creation wizard
  • AgentsListPage.tsx - List of user's created agents
  • ProfilePage.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> {
    const results = new Map();
    for (const url of urls) {
      try {
        const content = await this.scrapeWebsite(url);
        results.set(url, content);
      } catch (error) {
        console.error(`Failed to scrape ${url}:`, error);
      }
    }
    return results;
  }
}

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:

  1. Company Overview
  2. Products/Services
  3. Pricing Information
  4. Special Offers
  5. Company Values/Mission
  6. Contact Information
  7. Key Features/Benefits
  8. Target Audience
  9. Company Background
  10. 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:

  1. STAGE 1 — WELCOME: Greeting and establishing rapport
  2. STAGE 2 — SMALL TALK: Building trust and connection
  3. STAGE 3 — AGENDA: Setting meeting expectations
  4. STAGE 4 — QUALIFICATION: Discovery questions and needs analysis
  5. STAGE 5 — PRESENTATION: Value proposition and solution details
  6. STAGE 6 — PRICING: Pricing discussion and objection handling
  7. 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:

  1. User submits email, password, and name
  2. Backend hashes password with bcrypt (10 salt rounds)
  3. User record created with emailVerified: false
  4. Verification email sent with unique token
  5. User clicks verification link
  6. emailVerified set to true
  7. User can now log in

Login:

  1. User submits email and password
  2. Backend finds user by email
  3. Verifies email is confirmed
  4. Compares password hash with bcrypt
  5. Generates JWT access and refresh tokens
  6. Returns tokens and user data
  7. Frontend stores tokens in localStorage and sessionStorage

Password Reset:

  1. User requests password reset with email
  2. Backend generates unique reset token (UUID)
  3. Token stored in database with 1-hour expiration
  4. Reset email sent with link containing token
  5. User clicks link and enters new password
  6. Backend validates token and expiration
  7. Password hashed and updated
  8. Reset token cleared from database

Google OAuth Flow

  1. User clicks "Continue with Google" button
  2. Frontend redirects to /api/auth/google
  3. Backend initiates OAuth flow with Google
  4. User authenticates with Google
  5. Google redirects to /api/auth/google/callback
  6. Backend receives user profile from Google
  7. Check if user exists by googleId or email
  8. Create new user or update existing one
  9. Set emailVerified: true (Google pre-verifies emails)
  10. Generate JWT tokens
  11. Redirect to frontend with tokens in URL
  12. 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:

  1. Step 1: Username & Password
    POST /auth/login
    {
      "username": "admin",
      "password": "password"
    }
    
    // Response:
    {
      "requiresTwoFactor": true
    }
  2. 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: speakeasy library 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):

⚠️ Important: Change default credentials immediately after deployment!
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:

  1. Go to Google Account settings
  2. Enable 2-Factor Authentication
  3. Navigate to Security → App passwords
  4. Generate a new app password for "Mail"
  5. Copy the 16-character password
  6. Add to EMAIL_PASSWORD in .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.

Request Body:
{
  "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.

Request Body:
{
  "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.

Response:

Redirect to Google OAuth consent screen

GET /auth/google/callback

Google OAuth callback. Handled automatically by Passport.

Response:

Redirect to frontend with tokens in URL parameters

GET /auth/verify-email/:token

Verify user email address.

URL Parameters:
  • token - Verification token from email
Response (200 OK):
{
  "message": "Email verified successfully"
}
Errors:
  • 400: Invalid or expired token

POST /auth/forgot-password

Request password reset email.

Request Body:
{
  "email": "user@example.com"
}
Response (200 OK):
{
  "message": "Password reset email sent"
}

POST /auth/reset-password/:token

Reset password with token from email.

URL Parameters:
  • token - Reset token from email
Request Body:
{
  "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.

Headers:
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.

Headers:
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.

Headers:
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_FORMAL
  • FRIENDLY_CASUAL
  • TECHNICAL_EXPERT
  • CONSULTATIVE
Response (201 Created):
{
  "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.

Headers:
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.

Headers:
Authorization: Bearer {access_token}
URL Parameters:
  • id - Agent UUID
Response (200 OK):
{
  "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.

Headers:
Authorization: Bearer {access_token}
URL Parameters:
  • id - Agent UUID
Request Body (partial update):
{
  "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.

Headers:
Authorization: Bearer {access_token}
URL Parameters:
  • id - Agent UUID
Response (204 No Content):

Empty response body

Errors:
  • 401: Unauthorized
  • 404: Agent not found

POST /agents/:id/generate-description

Generate company description from website. Requires authentication.

Headers:
Authorization: Bearer {access_token}
URL Parameters:
  • id - Agent UUID
Request Body:
{
  "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.

Headers:
Authorization: Bearer {access_token}
URL Parameters:
  • id - Agent UUID
Request Body:
{
  "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"
  }'