A TypeScript + Bun powered notification service that sends templated emails using Nodemailer and schedules them with BullMQ + Valkey.
The service is exposed via a REST API (Hono) with internal authentication.
- ✅ Templated email rendering (
welcome,pre_post,post_success,post_error) - ✅ Queue-based job handling using BullMQ + Valkey
- ✅ Scheduled emails (using
delayin BullMQ) - ✅ Secure API with internal service authentication (
INTERNAL_API_TOKEN) - ✅ Centralized environment config loader with type safety
- ✅ Health check endpoint
src/
├── api/
│ ├── server.ts # REST API routes (Hono)
│ └── notify.ts # Notification route
├── core/
│ ├── queue.ts # BullMQ queues
│ ├── mailer.ts # Nodemailer wrapper
│ ├── templates.ts # Email templates + schema
│ └── auth.ts # Internal API auth middleware
├── utils/
│ ├── schemas.ts # NotificationSchema (Zod)
│ ├── logger.ts # Logger
│ └── config.ts # env loader (dotenv)
├── worker.ts # BullMQ worker (processes jobs)
└── index.ts # API server entrypoint
All variables are loaded and type-validated through src/utils/config.ts.
| Variable | Required | Description |
|---|---|---|
SMTP_HOST |
✅ | SMTP server host |
SMTP_PORT |
✅ | SMTP server port (default: 587) |
SMTP_USER |
✅ | SMTP username |
SMTP_PASS |
✅ | SMTP password |
QUEUE_HOST |
✅ | Valkey/Redis host |
QUEUE_PORT |
✅ | Valkey/Redis port (default: 6379) |
QUEUE_DB |
❌ | Redis DB index (default: 0) |
QUEUE_PASSWORD |
❌ | Redis password (if set) |
INTERNAL_API_TOKEN |
✅ | Secret token for internal API authentication |
VALID_SERVICES |
❌ | Comma-separated list of allowed service names (e.g. post_service,analytics_service) |
NODE_ENV |
❌ | Node environment (development, production) |
Example .env:
SMTP_HOST=smtp.mailtrap.io
SMTP_PORT=587
SMTP_USER=my_smtp_user
SMTP_PASS=my_smtp_pass
QUEUE_HOST=127.0.0.1
QUEUE_PORT=6379
QUEUE_DB=0
QUEUE_PASSWORD=
INTERNAL_API_TOKEN=supersecret
VALID_SERVICES=post_service,analytics_service
NODE_ENV=developmentbun installbun run devGET /Response:
{
"status": "ok",
"timestamp": "2025-08-24T10:00:00.000Z",
"uptime": "120 seconds"
}POST /api/v1/notify
x-service-key: <INTERNAL_API_TOKEN>
x-service-name: <SERVICE_NAME>
Content-Type: application/jsonExample payloads:
{
"to": "user@example.com",
"type": "welcome",
"data": { "user": "Priyanshu" },
"sendAt": "2025-08-25T10:00:00.000Z"
}{
"to": "user@example.com",
"type": "post_error",
"data": {
"user": "Priyanshu",
"postTitle": "10 Tips for LinkedIn Growth",
"error": "LinkedIn API returned 500"
}
}Response:
{ "status": "queued", "queueId": "bull:jobid123" }Build and run with Docker:
docker build -t notification-service .
docker run --env-file .env -p 3000:3000 notification-service-
The worker and server can run in the same process for simplicity, but in production you’ll typically scale workers separately.
-
For hosting:
- Server can run on Vercel (API only).
- Worker must run on a persistent container (Docker, Fly.io, Railway, etc.), since Vercel functions cannot run background jobs.