- Hero mockup: enhanced 3D perspective and shadow - Testimonials: illustrated SVG avatars - Growth pricing card: visual prominence (scale, gradient, badge) - Most Popular badge: repositioned to avoid overlapping heading - Nav: added Log In link next to Start Free Trial - Fixed btn-primary text colour on anchor tags (white on blue) - Fixed cursor: default on all non-interactive elements - Disabled user-select on non-form content to prevent text caret
11 KiB
TenderRadar Email Digest System
Overview
The email digest system automatically sends matched tender alerts to subscribed users. The system matches new tenders published in the last 24 hours against each user's saved preferences (keywords, sectors, value ranges, locations, authority types) and delivers personalized HTML email digests daily.
Architecture
Components
-
Database Schema: Uses existing
profilesandmatchestablesprofilestable stores user alert preferencesmatchestable tracks which tenders have been sent to which users
-
Email Digest Script:
/scripts/send-digest.js- Daily script that finds matching tenders for each user
- Sends professional HTML emails with tender details
- Marks matches as sent to avoid duplicates
- Supports dry-run mode for testing
-
API Endpoints: Alert preference management
GET /api/alerts/preferences- Retrieve user's alert settingsPOST /api/alerts/preferences- Create/update alert settings
-
Cron Job: Scheduled daily execution at 7am UTC
- Configured in
/etc/cron.d/(or user crontab) - Logs to
/home/peter/tenderpilot/digest.log
- Configured in
Database Schema
Profiles Table
Used to store user alert preferences. Required columns:
id: Primary keyuser_id: Foreign key to users table (unique constraint)keywords: TEXT[] - Array of keywords to match in tender title/descriptionsectors: TEXT[] - Array of sector/CPV codesmin_value: DECIMAL - Minimum tender value (GBP)max_value: DECIMAL - Maximum tender value (GBP)locations: TEXT[] - Array of location filtersauthority_types: TEXT[] - Array of authority type filterscreated_at: TIMESTAMP - Record creation timeupdated_at: TIMESTAMP - Last update time
Matches Table
Tracks which tenders have been sent to which users.
id: Primary keyuser_id: Foreign key to users tabletender_id: Foreign key to tenders tablesent: BOOLEAN - Whether email was sentcreated_at: TIMESTAMP- Unique constraint on (user_id, tender_id)
Configuration
Environment Variables
Add to .env:
# Email Digest Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=alerts@tenderradar.co.uk
SMTP_PASS=your-email-password
SMTP_FROM=TenderRadar Alerts <alerts@tenderradar.co.uk>
Note: Use placeholder values initially. Peter will update with production credentials.
SMTP Options
The script supports any SMTP provider. Common configurations:
Gmail:
- Host:
smtp.gmail.com - Port:
587(TLS) or465(SSL) - User: Your Gmail address
- Pass: App-specific password (generate in Google Account settings)
Office 365:
- Host:
smtp.office365.com - Port:
587(TLS) - User: Your Office 365 email
- Pass: Your Office 365 password
Generic SMTP:
- Update SMTP_HOST and SMTP_PORT accordingly
Usage
Running the Digest Script
Dry-run (test mode - no emails sent):
cd /home/peter/tenderpilot
node scripts/send-digest.js --dry-run
Output shows which users have matches and how many tenders match their preferences.
Production (sends emails):
cd /home/peter/tenderpilot
node scripts/send-digest.js
Automatic Execution
The cron job runs daily at 7am UTC:
0 7 * * * cd /home/peter/tenderpilot && node scripts/send-digest.js >> /home/peter/tenderpilot/digest.log 2>&1
View logs:
tail -f /home/peter/tenderpilot/digest.log
Manual Cron Management
View existing cron jobs:
crontab -l
Edit cron jobs:
crontab -e
Remove a cron job:
crontab -r
API Endpoints
GET /api/alerts/preferences
Retrieve the current user's alert preferences.
Request:
curl -H "Authorization: Bearer <token>" \
http://localhost:3456/api/alerts/preferences
Response:
{
"preferences": {
"id": 1,
"user_id": 5,
"keywords": ["infrastructure", "cleaning"],
"sectors": ["45000000"],
"min_value": 10000,
"max_value": 500000,
"locations": ["London", "Scotland"],
"authority_types": ["Local Authority", "NHS Trust"],
"created_at": "2026-02-14T12:00:00Z",
"updated_at": "2026-02-14T12:00:00Z"
}
}
Or if no preferences exist:
{
"preferences": null
}
POST /api/alerts/preferences
Create or update alert preferences for the current user.
Request:
curl -X POST -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"keywords": ["infrastructure", "cleaning"],
"sectors": ["45000000", "71000000"],
"min_value": 10000,
"max_value": 500000,
"locations": ["London", "Scotland"],
"authority_types": ["Local Authority", "NHS Trust"]
}' \
http://localhost:3456/api/alerts/preferences
Parameters:
keywords(array, optional): Keywords to match in tender title/descriptionsectors(array, optional): CPV codes or sector categoriesmin_value(number, optional): Minimum tender value (GBP)max_value(number, optional): Maximum tender value (GBP)locations(array, optional): Geographic locationsauthority_types(array, optional): Types of procuring authority
Response:
{
"preferences": {
"id": 1,
"user_id": 5,
"keywords": ["infrastructure", "cleaning"],
"sectors": ["45000000", "71000000"],
"min_value": 10000,
"max_value": 500000,
"locations": ["London", "Scotland"],
"authority_types": ["Local Authority", "NHS Trust"],
"created_at": "2026-02-14T12:00:00Z",
"updated_at": "2026-02-14T12:30:00Z"
},
"message": "Alert preferences updated successfully"
}
Email Template
The digest sends professional HTML emails with:
- TenderRadar branding header
- Summary of matched tenders count
- Table showing:
- Tender title and source
- Tender deadline
- Estimated value (£)
- Link to tender details
- Call-to-action button to dashboard
- Link to manage preferences
- TenderRadar footer with unsubscribe link
Emails are fully HTML-formatted with responsive design suitable for desktop and mobile clients.
Matching Algorithm
For each user, the script:
- Fetches all open tenders published in the last 24 hours
- Filters out tenders already sent to the user (using matches table)
- Applies user preference filters:
- Keywords: Matches against tender title + description (case-insensitive)
- Value Range: Filters by tender value_high >= min_value AND value_low <= max_value
- Locations: Matches against tender location
- Authority Types: Matches against tender authority_type
- Sectors: Matches CPV codes
- For matching tenders:
- Generates personalized HTML email
- Sends via SMTP
- Creates match record with sent=true
Note: All filters are AND conditions. If keywords are specified AND location is specified, tender must match BOTH criteria.
Troubleshooting
No emails being sent
- Check that users have verified emails:
SELECT * FROM users WHERE verified = true; - Check that users have preferences:
SELECT * FROM profiles; - Check that tenders exist:
SELECT * FROM tenders WHERE status = 'open' ORDER BY published_date DESC LIMIT 5; - Run dry-run to see matching logic:
node scripts/send-digest.js --dry-run - Check logs:
tail -100 digest.log
SMTP connection errors
- Verify credentials in
.env - Test SMTP connection manually (can use tools like
telnetornc) - Check firewall/network: ensure port is open outbound to SMTP server
- For Gmail: ensure "Less secure apps" is enabled or use App Password
- Check SMTP logs on server
Emails stuck in queue
- Check node process:
ps aux | grep node - Check for zombie processes:
ps aux | grep defunct - View recent logs:
tail -50 digest.log - Run script manually to see real-time errors
Database connection issues
- Verify DATABASE_URL in
.env - Test connection:
psql $DATABASE_URL -c "SELECT 1" - Check database is running:
sudo systemctl status postgresql - Check user permissions:
SELECT grantee, privilege_type FROM information_schema.role_table_grants WHERE table_name='profiles';
Monitoring
Key Metrics to Monitor
- Digest runs: Check cron execution with
grep send-digest digest.log - Email send rate: Count successful sends in logs
- Match rate: Ratio of tenders matched vs. users with preferences
- Error rate: Failed SMTP connections or database queries
- Database size: As matches table grows, consider archival/cleanup
Maintenance
Weekly:
- Review logs for errors:
grep -i "error\|failed" digest.log - Check disk space for logs:
du -sh digest.log
Monthly:
- Archive old logs:
gzip digest.log.* && mv digest.log.*.gz /archive/ - Verify cron job is still scheduled:
crontab -l - Test dry-run to ensure system is functional
Quarterly:
- Review SMTP provider limits and usage
- Check for database performance issues with large matches table
- Consider implementing match archival for old records
Future Enhancements
Potential improvements to the system:
- Frequency Options: Allow users to choose daily/weekly/instant digests
- Digest Format: Support plain text alternative to HTML
- Unsubscribe: Track unsubscribe preferences
- Match Scoring: Rank matches by relevance to user preferences
- Batch Sending: Use queue system (Bull, Bee-Queue) for high volume
- Analytics: Track open rates, click rates, conversion
- A/B Testing: Test different email templates
- Timezone Support: Send at user's local time, not UTC
- Webhook Delivery: Alternative to SMTP for certain providers
- Digest Personalization: Include user name, company, custom messages
File Structure
/home/peter/tenderpilot/
├── scripts/
│ └── send-digest.js # Main digest script
├── server.js # Updated with alert API endpoints
├── .env # Config (includes SMTP settings)
├── package.json # Updated with nodemailer dependency
├── digest.log # Output log (created on first run)
└── init-db.js # Database initialization
Dependencies
Required npm packages:
nodemailer: ^6.x - SMTP email sendingpg: ^8.x - PostgreSQL clientdotenv: ^16.x - Environment variable loading
All dependencies are already in package.json and node_modules.
Support & Questions
For issues or questions about the email digest system:
- Check logs:
tail -100 /home/peter/tenderpilot/digest.log - Run dry-run:
node scripts/send-digest.js --dry-run - Review this documentation
- Check database: Verify profiles and tenders exist
- Review API endpoint responses
Last Updated: 2026-02-14 System Version: 1.0