A production-grade email scheduling service with a React dashboard for scheduling, viewing, and managing bulk email sends at scale.
- Tech Stack
- Features Implemented
- Architecture Overview
- Prerequisites
- Quick Start
- Environment Variables
- Rate Limiting & Concurrency
- Persistence on Restart
- API Endpoints
- Project Structure
| Technology | Purpose |
|---|---|
| TypeScript | Type-safe JavaScript |
| Express.js | REST API framework |
| BullMQ | Job queue with Redis |
| Redis | Queue persistence + rate limiting |
| PostgreSQL (Neon) | Primary database |
| Drizzle ORM | Type-safe database queries |
| Passport.js | Google OAuth authentication |
| Nodemailer | Email sending via Ethereal |
| Technology | Purpose |
|---|---|
| React 18 | UI framework |
| TypeScript | Type safety |
| Vite | Build tool |
| Tailwind CSS v4 | Styling |
| React Router | Navigation |
| Lucide React | Icons |
| Technology | Purpose |
|---|---|
| Docker | Redis container |
| Ethereal Email | Fake SMTP for testing |
| Feature | Status | Description |
|---|---|---|
| Email Scheduling API | β | POST endpoint to schedule emails |
| BullMQ Delayed Jobs | β | No cron - pure queue-based scheduling |
| Redis Persistence | β | Jobs survive server restarts |
| Idempotency | β | Unique job IDs prevent duplicates |
| Worker Concurrency | β | Configurable via WORKER_CONCURRENCY |
| Rate Limiting | β | Redis-backed hourly limits |
| Delay Between Emails | β | 2-second minimum delay |
| Auto-Rescheduling | β | Jobs delayed to next hour when limit hit |
| Ethereal SMTP | β | Auto-generated test credentials |
| Google OAuth | β | Real authentication |
| Feature | Status | Description |
|---|---|---|
| Google Login | β | Real OAuth with avatar |
| Dashboard | β | Scheduled + Sent tabs |
| Compose Email | β | Rich text editor |
| CSV/TXT Upload | β | Parse email lists |
| File Attachments | β | Gmail-style chips |
| Schedule Picker | β | Date/time selection |
| Email List View | β | Search, filter, refresh |
| Email Detail View | β | Star, archive, delete |
| Loading States | β | Spinners |
| Empty States | β | Helpful messages |
| Logout | β | Session destruction |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FRONTEND (React) β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββββββββββββββ β
β β Login β β Dashboardβ β Compose β β Scheduled/Sent Lists β β
β ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ ββββββββββββ¬ββββββββββββ β
βββββββββΌββββββββββββββΌββββββββββββββΌβββββββββββββββββββββΌβββββββββββββ
β β β β
βΌ βΌ βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BACKEND (Express.js) β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββββββ β
β β Auth Routes β β Email Routes β β Rate Limiter β β
β β /api/auth β β /api/emails β β (Redis Counters) β β
β ββββββββββββββββ ββββββββ¬ββββββββ ββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββ
βΌ βΌ βΌ
ββββββββββββββββ ββββββββββββββββββ ββββββββββββββββ
β PostgreSQL β β Redis β β BullMQ β
β (Neon) β β (Docker) β β Worker β
β β β β β β
β β’ users β β β’ Job Queue β β β’ Processes β
β β’ emails β β β’ Rate Limits β β delayed β
β β β β’ Sessions β β jobs β
ββββββββββββββββ ββββββββββββββββββ ββββββββ¬ββββββββ
β
βΌ
ββββββββββββββββ
β Ethereal β
β SMTP β
β (Testing) β
ββββββββββββββββ
- User schedules email β Frontend calls
POST /api/emails/schedule - Backend stores in PostgreSQL β Email record with
status: 'scheduled' - Job added to BullMQ β Delayed job with calculated delay in milliseconds
- At scheduled time β Worker picks up job from Redis queue
- Rate limit check β Redis counter for current hour
- If allowed β Send via Ethereal SMTP, update status to
sent - If rate limited β Reschedule to next hour window
SERVER STOPS
β
βΌ
βββββββββββββββββββ
β Redis persists β β Jobs saved in Redis RDB/AOF
β all BullMQ β
β delayed jobs β
βββββββββββββββββββ
β
SERVER RESTARTS
β
βΌ
βββββββββββββββββββ
β Worker reconnectsβ β BullMQ auto-resumes
β to Redis queue β processing delayed jobs
βββββββββββββββββββ
β
βΌ
Emails send at
correct scheduled time β
- Node.js 18+
- Docker Desktop (for Redis)
- Neon PostgreSQL account (free tier)
- Google Cloud Console project (for OAuth)
git clone https://github.com/aryanrai97861/ReachInbox.git
cd ReachInboxdocker-compose up -dVerify it's running:
docker ps
# Should show: reachinbox-rediscd backend
npm install
# Create environment file
cp .env.example .env
# Edit .env with your credentials (see Environment Variables section)
# Push database schema to Neon
npm run db:push
# Start backend server
npm run devBackend runs on: http://localhost:3001
cd frontend
npm install
npm run devFrontend runs on: http://localhost:5173
Navigate to http://localhost:5173 and login with Google!
# Database (Neon PostgreSQL)
DATABASE_URL=postgresql://username:password@host/database?sslmode=require
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# Google OAuth (from Google Cloud Console)
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Session
SESSION_SECRET=your-random-secret-key-min-32-chars
# Worker Configuration
WORKER_CONCURRENCY=5
MAX_EMAILS_PER_HOUR=200
EMAIL_DELAY_MS=2000
# Server
PORT=3001
FRONTEND_URL=http://localhost:5173- Go to neon.tech
- Create a free project
- Copy the connection string
- Go to Google Cloud Console
- Create a new project
- Enable "Google+ API"
- Go to Credentials β Create OAuth 2.0 Client ID
- Add authorized redirect URI:
http://localhost:3001/api/auth/google/callback - Copy Client ID and Client Secret
- Auto-generated! No setup needed
- Credentials are logged to console when backend starts
- View sent emails at: https://ethereal.email
| Parameter | Default | Config Variable |
|---|---|---|
| Emails per hour | 200 | MAX_EMAILS_PER_HOUR |
| Delay between emails | 2 seconds | EMAIL_DELAY_MS |
| Worker concurrency | 5 | WORKER_CONCURRENCY |
// Redis key format: ratelimit:YYYY-MM-DD:HH
// Example: ratelimit:2024-01-13:22
// Before sending each email:
1. GET current count from Redis
2. If count < MAX_EMAILS_PER_HOUR β Send email, INCR counter
3. If count >= MAX_EMAILS_PER_HOUR β Reschedule to next hourWhen 1000 emails are scheduled for the same time:
- First 200 emails β Sent immediately (within rate limit)
- Next 200 emails β Rescheduled to Hour+1
- Next 200 emails β Rescheduled to Hour+2
- And so on...
Jobs are never dropped - they're automatically delayed to the next available hour window.
- Rate limit counters use Redis INCR (atomic operation)
- Safe for multiple workers/instances
- BullMQ's limiter provides additional throttling
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/auth/google |
Initiate Google OAuth |
| GET | /api/auth/google/callback |
OAuth callback |
| GET | /api/auth/me |
Get current user |
| POST | /api/auth/logout |
Logout |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/emails/schedule |
Schedule emails |
| GET | /api/emails/scheduled |
List scheduled emails |
| GET | /api/emails/sent |
List sent emails |
| GET | /api/emails/stats |
Queue statistics |
POST /api/emails/schedule
{
"subject": "Hello!",
"body": "<b>HTML content</b>",
"recipients": ["user1@example.com", "user2@example.com"],
"scheduledAt": "2024-01-14T10:00:00.000Z",
"delayBetweenEmails": 2000,
"hourlyLimit": 200
}ReachInbox/
βββ backend/
β βββ src/
β β βββ config/ # Redis, Email config
β β βββ db/ # Drizzle schema & connection
β β βββ middleware/ # Auth middleware
β β βββ queues/ # BullMQ queue & worker
β β βββ routes/ # API routes
β β βββ services/ # Rate limiter
β β βββ index.ts # Entry point
β βββ drizzle.config.ts
β βββ package.json
β
βββ frontend/
β βββ src/
β β βββ components/ # Reusable UI components
β β βββ hooks/ # Custom React hooks
β β βββ pages/ # Page components
β β βββ services/ # API client
β β βββ types/ # TypeScript interfaces
β β βββ App.tsx # Router setup
β βββ package.json
β
βββ docker-compose.yml # Redis container
βββ README.md
- Ethereal Email: Used as per requirements - emails are NOT delivered to real mailboxes
- Single Sender: Currently uses one Ethereal account (can be extended for multi-sender)
- Global Rate Limit: Implemented globally, not per-sender (simpler for demo)
- In-Memory Session Backup: Sessions stored in Redis for persistence
- Frontend Polling: Dashboard refreshes every 5 seconds (could use WebSockets)
MIT