An intelligent Request for Proposal (RFP) management system that automates the creation, distribution, and analysis of RFPs using AI-powered natural language processing. The system automatically processes vendor proposals received via email and provides AI-driven comparison and recommendations.
- Features
- Tech Stack
- Prerequisites
- Project Setup
- Configuration
- Running the Application
- API Documentation
- Design Decisions & Assumptions
- Troubleshooting
- Natural Language RFP Creation: Create structured RFPs from plain text descriptions using AI
- Automated Email Distribution: Send RFPs to multiple vendors via email
- Intelligent Email Processing: Automatically poll and process vendor proposal emails
- AI-Powered Proposal Extraction: Extract structured data from vendor proposals
- Smart Comparison: AI-driven comparison of proposals based on price, delivery, and completeness
- Vendor Management: Manage vendor database with contact information
- Real-time Dashboard: Track RFPs, proposals, and vendor responses
- Framework: React 18.2 with TypeScript
- Build Tool: Vite 5.0
- Routing: React Router DOM 6.20
- Styling: Tailwind CSS 3.4
- Icons: Lucide React
- Runtime: Node.js (TypeScript)
- Framework: Express 4.18
- Database: PostgreSQL 8.16
- AI Provider: OpenAI GPT-4o-mini
- Email:
- SMTP: Nodemailer 6.9
- IMAP: node-imap 0.8
- Parsing: mailparser 3.6
- cors: Cross-origin resource sharing
- dotenv: Environment variable management
- pg: PostgreSQL client
- openai: OpenAI API client
- ts-node-dev: Development server with hot reload
Before setting up the project, ensure you have:
- Node.js: Version 18.x or higher
- npm: Version 9.x or higher (comes with Node.js)
- PostgreSQL: Version 12.x or higher
- OpenAI API Key: Required for AI-powered features
- Email Account: Gmail or compatible SMTP/IMAP email account with:
- SMTP access enabled
- IMAP access enabled
- App-specific password (if using Gmail with 2FA)
If using Gmail:
- Enable 2-Factor Authentication on your Google account
- Generate an App Password:
- Go to Google Account Settings → Security
- Under "Signing in to Google", select "App passwords"
- Generate a new app password for "Mail"
- Use this password in your
.envfile
git clone <repository-url>
cd rfp-management-systemcd backend
npm installcd frontend
npm install# Connect to PostgreSQL
psql -U postgres
# Create database
CREATE DATABASE rfp_management;
# Exit psql
\qcd backend
npm run db:initThis will create all necessary tables, indexes, and triggers defined in backend/src/config/schema.sql.
Create backend/.env file from the example:
cd backend
cp .env.example .envEdit backend/.env with your configuration:
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=rfp_management
DB_USER=postgres
DB_PASSWORD=your_postgres_password
# Server Configuration
PORT=3000
# OpenAI API Configuration
OPENAI_API_KEY=sk-your-openai-api-key-here
AI_TIMEOUT_MS=30000
AI_MAX_RETRIES=3
# Email Service Configuration (Gmail example)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-specific-password
# IMAP Configuration
IMAP_HOST=imap.gmail.com
IMAP_PORT=993
IMAP_USER=your-email@gmail.com
IMAP_PASSWORD=your-app-specific-password
IMAP_TLS=true
# Email Polling Configuration
EMAIL_POLL_INTERVAL_MS=60000
EMAIL_FROM=your-email@gmail.com
EMAIL_REPLY_TO=your-email@gmail.comCreate frontend/.env file from the example:
cd frontend
cp .env.example .envEdit frontend/.env:
# Backend API URL
VITE_API_URL=http://localhost:3000
# API Timeout (milliseconds)
VITE_API_TIMEOUT=30000cd backend
npm run devThe backend server will start on http://localhost:3000 (or the PORT specified in .env).
Features:
- Hot reload on file changes
- Email polling service starts automatically
- Polls for new emails every 60 seconds (configurable)
cd frontend
npm run devThe frontend will start on http://localhost:5173 (Vite default).
cd backend
npm run build
npm startcd frontend
npm run build
npm run previewTo populate the database with sample data:
cd backend
npm run seedThis will create:
- Sample vendors
- Sample RFPs
- Sample proposals
Base URL: http://localhost:3000/api
Check if the API is running.
Response:
{
"status": "ok"
}Create a new vendor.
Request Body:
{
"name": "Acme Corporation",
"email": "contact@acme.com",
"contactPerson": "John Doe",
"phone": "+1-555-0123",
"address": "123 Business St, City, State 12345"
}Success Response (201):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corporation",
"email": "contact@acme.com",
"contactPerson": "John Doe",
"phone": "+1-555-0123",
"address": "123 Business St, City, State 12345",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}Error Response (400):
{
"error": "Validation failed: email is required"
}List all vendors with pagination.
Query Parameters:
page(optional): Page number (default: 1)limit(optional): Items per page (default: 50, max: 100)
Success Response (200):
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corporation",
"email": "contact@acme.com",
"contactPerson": "John Doe",
"phone": "+1-555-0123",
"address": "123 Business St, City, State 12345",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 1,
"totalPages": 1
}
}Get vendor details by ID.
Success Response (200):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corporation",
"email": "contact@acme.com",
"contactPerson": "John Doe",
"phone": "+1-555-0123",
"address": "123 Business St, City, State 12345",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}Error Response (404):
{
"error": "Vendor not found: 550e8400-e29b-41d4-a716-446655440000"
}Update vendor information.
Request Body:
{
"name": "Acme Corporation Ltd",
"contactPerson": "Jane Smith"
}Success Response (200):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corporation Ltd",
"email": "contact@acme.com",
"contactPerson": "Jane Smith",
"phone": "+1-555-0123",
"address": "123 Business St, City, State 12345",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T11:00:00.000Z"
}Delete a vendor.
Success Response (204): No content
Error Response (404):
{
"error": "Vendor not found: 550e8400-e29b-41d4-a716-446655440000"
}Create an RFP from natural language description.
Request Body:
{
"description": "We need 50 laptops with 16GB RAM and 512GB SSD. Budget is $50,000 USD. Delivery needed within 30 days. Payment terms: Net 30. Warranty: 3 years minimum."
}Success Response (201):
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"description": "We need 50 laptops...",
"structuredData": {
"items": [
{
"name": "Laptop",
"quantity": 50,
"specifications": "16GB RAM, 512GB SSD"
}
],
"budget": 50000,
"currency": "USD",
"deliveryTimeline": "30 days",
"paymentTerms": "Net 30",
"warrantyRequirements": "3 years minimum",
"additionalTerms": null
},
"status": "draft",
"createdAt": "2024-01-15T10:30:00.000Z",
"sentAt": null
}Error Response (400):
{
"error": "Description is required"
}List all RFPs with pagination.
Query Parameters:
page(optional): Page number (default: 1)limit(optional): Items per page (default: 50, max: 100)
Success Response (200):
{
"data": [
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"description": "We need 50 laptops...",
"structuredData": { ... },
"status": "sent",
"createdAt": "2024-01-15T10:30:00.000Z",
"sentAt": "2024-01-15T11:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 1,
"totalPages": 1
}
}Get RFP details with vendors and proposals.
Success Response (200):
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"description": "We need 50 laptops...",
"structuredData": { ... },
"status": "sent",
"createdAt": "2024-01-15T10:30:00.000Z",
"sentAt": "2024-01-15T11:00:00.000Z",
"vendors": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corporation",
"email": "contact@acme.com",
"sentAt": "2024-01-15T11:00:00.000Z"
}
],
"proposals": [
{
"id": "770e8400-e29b-41d4-a716-446655440002",
"rfpId": "660e8400-e29b-41d4-a716-446655440001",
"vendorId": "550e8400-e29b-41d4-a716-446655440000",
"vendorName": "Acme Corporation",
"extractedData": { ... },
"completenessScore": 0.95,
"aiSummary": "Strong proposal with competitive pricing...",
"receivedAt": "2024-01-16T09:00:00.000Z",
"processingStatus": "completed"
}
]
}Send RFP to selected vendors via email.
Request Body:
{
"vendorIds": [
"550e8400-e29b-41d4-a716-446655440000",
"550e8400-e29b-41d4-a716-446655440001"
]
}Success Response (200):
{
"message": "RFP sent successfully",
"rfp": {
"id": "660e8400-e29b-41d4-a716-446655440001",
"status": "sent",
"sentAt": "2024-01-15T11:00:00.000Z"
},
"results": [
{
"vendorId": "550e8400-e29b-41d4-a716-446655440000",
"vendorEmail": "contact@acme.com",
"success": true,
"sentAt": "2024-01-15T11:00:00.000Z"
}
]
}Partial Failure Response (207):
{
"message": "RFP sent with some failures",
"rfp": { ... },
"results": [
{
"vendorId": "550e8400-e29b-41d4-a716-446655440000",
"vendorEmail": "contact@acme.com",
"success": true,
"sentAt": "2024-01-15T11:00:00.000Z"
},
{
"vendorId": "550e8400-e29b-41d4-a716-446655440001",
"vendorEmail": "invalid@vendor.com",
"success": false,
"error": "SMTP connection failed"
}
]
}Get all proposals for an RFP with pagination.
Query Parameters:
page(optional): Page number (default: 1)limit(optional): Items per page (default: 50, max: 100)
Success Response (200):
{
"data": [
{
"id": "770e8400-e29b-41d4-a716-446655440002",
"rfpId": "660e8400-e29b-41d4-a716-446655440001",
"vendorId": "550e8400-e29b-41d4-a716-446655440000",
"vendorName": "Acme Corporation",
"extractedData": {
"items": [
{
"name": "Laptop",
"unitPrice": 950,
"totalPrice": 47500,
"specifications": "16GB RAM, 512GB SSD, Intel i7"
}
],
"totalPrice": 47500,
"currency": "USD",
"deliveryTimeline": "25 days",
"paymentTerms": "Net 30",
"warrantyOffered": "3 years comprehensive",
"additionalConditions": "Free shipping included"
},
"completenessScore": 0.95,
"aiSummary": "Strong proposal with competitive pricing...",
"receivedAt": "2024-01-16T09:00:00.000Z",
"processingStatus": "completed"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 1,
"totalPages": 1
}
}Get AI-powered comparison of all proposals for an RFP.
Success Response (200):
{
"rfpId": "660e8400-e29b-41d4-a716-446655440001",
"proposals": [ ... ],
"analysis": {
"priceComparison": [
{
"vendorId": "550e8400-e29b-41d4-a716-446655440000",
"vendorName": "Acme Corporation",
"totalPrice": 47500,
"priceRank": 1
}
],
"deliveryComparison": [
{
"vendorId": "550e8400-e29b-41d4-a716-446655440000",
"vendorName": "Acme Corporation",
"deliveryDays": 25,
"deliveryRank": 1
}
],
"completenessComparison": [
{
"vendorId": "550e8400-e29b-41d4-a716-446655440000",
"vendorName": "Acme Corporation",
"score": 95,
"missingItems": []
}
]
},
"recommendation": {
"recommendedVendorId": "550e8400-e29b-41d4-a716-446655440000",
"reasoning": "Acme Corporation offers the best overall value with competitive pricing, fast delivery, and complete proposal coverage.",
"alternativeOptions": "Consider Vendor B if faster delivery is critical."
},
"generatedAt": "2024-01-16T10:00:00.000Z"
}Error Response (404):
{
"error": "No proposals found for RFP: 660e8400-e29b-41d4-a716-446655440001"
}Webhook endpoint for processing incoming proposal emails.
Request Body:
{
"from": "vendor@acme.com",
"to": "rfp@yourcompany.com",
"subject": "Re: Request for Proposal - 660e8400-e29b-41d4-a716-446655440001",
"body": "We are pleased to submit our proposal...",
"htmlBody": "<html>...</html>",
"attachments": []
}Success Response (201):
{
"message": "Email processed successfully",
"proposal": {
"id": "770e8400-e29b-41d4-a716-446655440002",
"rfpId": "660e8400-e29b-41d4-a716-446655440001",
"vendorId": "550e8400-e29b-41d4-a716-446655440000",
"extractedData": { ... },
"completenessScore": 0.95,
"aiSummary": "Strong proposal...",
"receivedAt": "2024-01-16T09:00:00.000Z",
"processingStatus": "completed"
}
}Error Response (400):
{
"error": "Could not match email to RFP"
}- Decision: Use OpenAI GPT-4o-mini for all natural language processing tasks
- Reasoning:
- Provides flexibility for handling various RFP formats
- Reduces need for rigid parsing rules
- Enables intelligent comparison and recommendations
- Trade-offs:
- Requires API key and incurs costs
- Dependent on external service availability
- Response times vary (30s timeout configured)
- Decision: Implemented IMAP polling for incoming emails
- Reasoning:
- Simpler setup without requiring public endpoints
- Works with any IMAP-compatible email provider
- No need for webhook configuration or SSL certificates
- Trade-offs:
- Slight delay in processing (60-second poll interval)
- More resource-intensive than webhooks
- Alternative: Could implement webhook support for real-time processing
- Decision: Used repository pattern for data access
- Reasoning:
- Separates business logic from data access
- Makes testing easier with dependency injection
- Allows easy database migration if needed
- Implementation: BaseRepository with specialized repositories for each entity
- Decision: Store RFP structured data and proposal extracted data as JSONB
- Reasoning:
- Flexible schema for varying RFP requirements
- Efficient querying with PostgreSQL JSONB operators
- Reduces need for complex relational schemas
- Trade-offs: Less type safety, requires validation at application level
- Decision: AI-generated completeness scores (0-1 scale)
- Reasoning:
- Provides quick assessment of proposal quality
- Helps identify missing information
- Enables ranking and comparison
- Calculation: Based on how well proposal addresses RFP requirements
-
RFP ID in Email: Vendors will include the RFP ID in their response email (subject or body)
- System looks for patterns: "RFP ID: xxx" or "Reference RFP ID: xxx"
- Falls back to dead letter queue if no match found
-
Email Format: Vendor proposals can be in various formats (plain text, HTML, tables)
- AI service is flexible enough to handle different formats
- Attachments are stored but not currently processed
-
Single Proposal per Email: Each email contains one proposal for one RFP
- Multiple proposals require multiple emails
-
Vendor Email Matching: Vendor is identified by email address
- Email must match a vendor in the database
- Case-insensitive matching
-
Natural Language Input: Users provide RFP details in natural language
- AI extracts structured data
- Missing fields are flagged for user review
-
Currency Standardization: All prices in single currency per RFP
- No automatic currency conversion
-
Item Specifications: Flexible text field for specifications
- No rigid structure enforced
-
Comparable Proposals: All proposals for an RFP are comparable
- Same currency assumed
- Similar item structures
-
Delivery Timeline: Expressed in days for comparison
- AI converts various formats ("2 weeks", "30 days") to days
-
Price Ranking: Lower price is better (rank 1 = lowest)
- Doesn't account for quality differences
-
Raw Email Storage: All incoming emails stored indefinitely
- Enables reprocessing if needed
- Audit trail for proposals
-
No Automatic Deletion: RFPs, proposals, and vendors are never auto-deleted
- Manual cleanup required
-
No Authentication: Current implementation has no user authentication
- Suitable for internal/demo use only
- Production deployment requires auth layer
-
Email Credentials: Stored in environment variables
- Should use secrets management in production
-
API Access: No rate limiting or API keys
- Open access to all endpoints
-
Email Provider: Tested primarily with Gmail
- Other providers may require configuration adjustments
-
Attachment Processing: Attachments are stored but not analyzed
- Future enhancement: Extract data from PDF/Excel attachments
-
Concurrent Email Processing: Sequential processing of emails
- Could be parallelized for better performance
-
AI Timeout: 30-second timeout for AI operations
- Very large RFPs or many proposals may timeout
- Configurable via AI_TIMEOUT_MS
-
No Real-time Updates: Frontend requires manual refresh
- Could implement WebSocket for real-time updates
-
Single Language: English only
- AI could support other languages with prompt modifications
Problem: Cannot connect to PostgreSQL
Solutions:
-
Verify PostgreSQL is running:
# Windows pg_ctl status # Linux/Mac sudo systemctl status postgresql
-
Check database credentials in
.env -
Ensure database exists:
psql -U postgres -l
-
Test connection:
psql -U postgres -d rfp_management
Problem: Cannot send or receive emails
Solutions:
- Verify SMTP/IMAP credentials in
.env - For Gmail:
- Enable 2FA
- Generate app-specific password
- Enable "Less secure app access" (if not using app password)
- Check firewall settings for ports 587 (SMTP) and 993 (IMAP)
- Test SMTP connection:
telnet smtp.gmail.com 587
Problem: AI operations failing or timing out
Solutions:
- Verify API key is correct in
.env - Check OpenAI account has credits
- Increase timeout:
AI_TIMEOUT_MS=60000
- Reduce input size (shorter RFP descriptions)
- Check OpenAI status: https://status.openai.com
Problem: Incoming emails not being processed
Solutions:
- Check backend logs for errors
- Verify IMAP credentials
- Manually test email fetch:
- Send test email to configured address
- Check if email appears in inbox
- Wait for poll interval (default 60s)
- Reduce poll interval for testing:
EMAIL_POLL_INTERVAL_MS=10000
Problem: API calls failing from frontend
Solutions:
- Verify backend is running on correct port
- Check
VITE_API_URLinfrontend/.env - Verify CORS is enabled in backend
- Check browser console for errors
- Test API directly:
curl http://localhost:3000/api/health
Problem: TypeScript compilation errors
Solutions:
-
Clear node_modules and reinstall:
rm -rf node_modules package-lock.json npm install
-
Check Node.js version:
node --version # Should be 18.x or higher -
Clear TypeScript cache:
rm -rf dist npm run build
Used ChatGPT for generating boilerplate code, resolving bugs more efficiently, and producing comprehensive documentation.
Note: This is a demonstration/development system. For production use, implement proper authentication, authorization, secrets management, monitoring, and error handling.