A high-deliverability bulk email service built with Node.js, TypeScript, Fastify, PostgreSQL, Redis, BullMQ, and AWS SES.
✅ High Deliverability - SPF, DKIM, DMARC support
✅ Queue-Based Processing - BullMQ + Redis for reliable delivery
✅ Email Suppression - Automatic bounce/complaint handling
✅ Retry Logic - Exponential backoff with configurable attempts
✅ Dead Letter Queue - Automatic DLQ for failed jobs
✅ API Authentication - Secure API key management
✅ Request Validation - Zod schema validation
✅ Health Checks - Liveness & readiness endpoints
✅ Structured Logging - Pino logger with pretty printing
✅ Comprehensive Metrics - Built-in metrics tracking
✅ Graceful Shutdown - Clean worker/server termination
✅ Docker Ready - Production-grade multi-stage build
- Node.js 20+
- PostgreSQL 15+
- Redis 7+
- Docker & Docker Compose (optional)
# Clone and install
cd mailer
npm install
# Set up environment
cp .env.example .env
# Edit .env with your AWS SES credentials and database URL# Run migrations
npm run prisma:migrate
# Seed test data
npm run seedThis creates a test API key and sample template.
Option A: Local Development
# Terminal 1: API Server
npm run dev:api
# Terminal 2: Email Worker
npm run dev:worker
# Terminal 3: Redis (separate process)
redis-server
# Terminal 4: PostgreSQL (separate process)
postgres -D /usr/local/var/postgresOption B: Docker Compose
docker-compose upAll API endpoints (except /health, /ready) require Bearer token authentication:
Authorization: Bearer YOUR_API_KEYGET /api/v1/health # Liveness check
GET /api/v1/ready # Readiness check (migrations + connections)
GET /api/v1/metrics # Public metrics dashboard# List templates
GET /api/v1/templates
Authorization: Bearer <key>
# Create template
POST /api/v1/templates
Authorization: Bearer <key>
Content-Type: application/json
{
"name": "Welcome Email",
"html": "<h1>Welcome {{name}}!</h1>",
"text": "Welcome {{name}}!",
"variables": {
"name": { "type": "string" }
}
}
# Get specific template
GET /api/v1/templates/:id
Authorization: Bearer <key>
# Delete template
DELETE /api/v1/templates/:id
Authorization: Bearer <key># Create batch and queue emails
POST /api/v1/batches
Authorization: Bearer <key>
Content-Type: application/json
{
"template_id": "cl...",
"recipients": [
{
"email": "user1@example.com",
"variables": { "name": "John" }
},
{
"email": "user2@example.com",
"variables": { "name": "Jane" }
}
],
"metadata": {
"campaign": "welcome_2024"
}
}
# Response: 201 Created
{
"id": "batch_id",
"status": "SENDING",
"createdAt": "2024-02-14T..."
}
# Get batch status
GET /api/v1/batches/:id
Authorization: Bearer <key>
# Get batch events (sent/failed/bounced)
GET /api/v1/batches/:id/events?limit=100&offset=0
Authorization: Bearer <key>
# Get batch summary
GET /api/v1/batches/:id/summary
Authorization: Bearer <key># SES Event Notifications (SNS → HTTP)
POST /api/v1/webhooks/ses
Content-Type: application/json
{
"bounce": {
"bounceType": "Permanent",
"bouncedRecipients": [
{
"emailAddress": "invalid@example.com",
"status": "5.1.1"
}
]
}
}# Generate API key
POST /api/v1/admin/keys
Content-Type: application/json
{
"name": "Mobile App"
}
# Response: 201 Created
{
"id": "key_id",
"key": "64hex_characters", // Only returned once!
"name": "Mobile App",
"createdAt": "2024-02-14T..."
}
# List API keys
GET /api/v1/admin/keys
# Revoke API key
POST /api/v1/admin/keys/:id/revoke
# Get metrics
GET /api/v1/admin/metrics?hours=24
# Manage suppressions
GET /api/v1/admin/suppressions?limit=100&offset=0
POST /api/v1/admin/suppressions
DELETE /api/v1/admin/suppressions/:email# App
NODE_ENV=production
PORT=3000
# Database
DATABASE_URL=postgres://user:password@host:5432/mailer
# Redis
REDIS_URL=redis://host:6379
# AWS SES
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_key
AWS_SECRET_ACCESS_KEY=your_secret
MAIL_FROM=no-reply@yourdomain.com
# Worker
WORKER_CONCURRENCY=10 # Parallel email sends
JOB_ATTEMPTS=3 # Retry attempts
JOB_BACKOFF_DELAY=5000 # Exponential backoff in ms-
Verify Domain Identity
aws ses verify-domain-identity --domain yourdomain.com --region us-east-1
-
Configure DKIM
aws ses verify-domain-dkim --domain yourdomain.com --region us-east-1
-
Add DNS Records (DKIM tokens from above)
DKIM CNAME Records: token1._domainkey.yourdomain.com CNAME token1.xx.amazonses.com token2._domainkey.yourdomain.com CNAME token2.xx.amazonses.com token3._domainkey.yourdomain.com CNAME token3.xx.amazonses.com SPF Record: v=spf1 include:amazonses.com ~all DMARC Record (optional but recommended): v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com -
Request Production Access
By default, SES accounts are in sandbox mode and can only send to verified addresses. Request production access in the SES console.
-
Set Up SNS Notifications (for bounces/complaints)
# Create SNS topic aws sns create-topic --name mailer-events --region us-east-1 # Subscribe HTTP endpoint aws sns subscribe --topic-arn arn:aws:sns:us-east-1:xxx:mailer-events \ --protocol https \ --notification-endpoint https://yourdomain.com/api/v1/webhooks/ses # Configure SES to publish to SNS aws ses set-identity-notification-topic \ --identity yourdomain.com \ --identity-type Domain \ --notification-type Bounce \ --sns-topic arn:aws:sns:us-east-1:xxx:mailer-events
# Generate API key
npm run gen-key "Production Key"
# Seed database with test data
npm run seed
# Clean up dead letter queue
npm run cleanup-dlq
# Build for production
npm run build
# Start production server
npm run start
npm run start:worker- Template - Email templates with Handlebars variables
- Batch - Groups of emails with status tracking
- Recipient - Individual email target + variables
- Event - Email lifecycle events (SENT, FAILED, BOUNCE, COMPLAINT)
- Suppression - Bounced/complained emails (auto-managed via webhook)
- ApiKey - API credentials with usage tracking
- Metric - Event metrics for monitoring
# Build image
docker build -t mailer:latest .
# Run with docker-compose
docker-compose up -d
# View logs
docker-compose logs -f api
docker-compose logs -f worker
# Stop services
docker-compose downapiVersion: apps/v1
kind: Deployment
metadata:
name: mailer-api
spec:
replicas: 3
template:
spec:
containers:
- name: api
image: mailer:latest
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: mailer-secrets
key: database-url
ports:
- containerPort: 3000
livenessProbe:
httpGet:
path: /api/v1/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/v1/ready
port: 3000
initialDelaySeconds: 20
periodSeconds: 5# System metrics
curl http://localhost:3000/api/v1/metrics
# Admin metrics (24h)
curl -H "Authorization: Bearer YOUR_KEY" \
http://localhost:3000/api/v1/admin/metrics?hours=24
# Check suppression list size
curl -H "Authorization: Bearer YOUR_KEY" \
http://localhost:3000/api/v1/admin/suppressions- AWS SES verified domain (SPF, DKIM, DMARC)
- SNS → Webhook configured for bounce/complaint handling
- PostgreSQL with automated backups
- Redis persistence enabled (
appendonly yes) - API keys stored securely (use env vars)
- Environment validation passing
- Logging aggregation (CloudWatch, DataDog, etc.)
- Rate limiting configured for API
- Database connection pooling tuned
- Health checks monitored
- Graceful shutdown tested
- Error alerts configured
- Check worker logs:
docker-compose logs worker - Verify SES credentials:
aws ses send-email --cli-input-json file://test.json - Check suppression list:
GET /api/v1/admin/suppressions - Review batch events:
GET /api/v1/batches/:id/events
- Increase
WORKER_CONCURRENCY(watch CPU/memory) - Scale horizontally - add more worker containers
- Check Redis connection pool
- Monitor database query performance
- Review failed job reasons:
npm run cleanup-dlq - Adjust retry policy:
JOB_ATTEMPTS,JOB_BACKOFF_DELAY - Check recipient email validity
-- Index batch queries
CREATE INDEX idx_batch_status ON batch(status);
CREATE INDEX idx_event_batch_type ON event(batch_id, type);
CREATE INDEX idx_suppression_created ON suppression(created_at DESC);
-- Connection pool info
SELECT datname, count(*) FROM pg_stat_activity GROUP BY datname;Issues and PRs welcome! Ensure:
- TypeScript strict mode passes
- All tests pass
- Environment validation succeeds
- Code follows existing patterns
MIT
For bugs/features, open an issue or contact support@yourdomain.com.