A generic, multi-channel notification gateway for microservices.
Chapar (Persian: چاپار — courier) is an open-source notification gateway built with NestJS. It accepts notification requests (SMS, Email, Push) from other services and reliably dispatches them through pluggable providers, with queued delivery, automatic retries, and full audit history.
- Multi-channel — SMS, Email, and Push out of the box.
- Strategy-pattern providers — add a new channel by implementing one interface; no core changes required.
- Pluggable push delivery — Firebase Cloud Messaging or in-app delivery via Redis Pub/Sub, switchable by config.
- Reliable queueing — BullMQ + Redis with exponential backoff and configurable retries.
- Template management — Handlebars templates rendered from
{ template, data }payloads. - Audit trail — every attempt is persisted to PostgreSQL (
PENDING→SENT/FAILED). - Observability — Prometheus metrics at
/metrics. - Production-ready — Docker, Docker Compose, and Kubernetes manifests included.
- Secure by default — API-key auth, rate limiting, Helmet headers, strict input validation.
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
POST /notify ──▶│ API (NestJS)│─────▶│ BullMQ/Redis│─────▶│ Worker(s) │
│ validate + │ enqueue │ pick │ render tmpl │
│ persist log │ └─────────────┘ job │ + send via │
└──────┬───────┘ │ Provider │
│ └──────┬───────┘
▼ ▼
┌──────────────┐ ┌──────────────┐
│ PostgreSQL │◀─────── status update ───│ SMS / Email │
│ (audit logs) │ │ / Push │
└──────────────┘ └──────────────┘
The API layer and workers scale independently — spin up more workers to drain a large queue without touching the API.
The same image runs in one of three roles, selected by the CHAPAR_ROLE env var:
| Role | Serves HTTP (/notify, /logs) |
Consumes the queue (sends) | Use it for |
|---|---|---|---|
api |
✅ | ❌ | Public ingress, scale with traffic |
worker |
health/metrics only | ✅ | Sending, scale with throughput |
all (default) |
✅ | ✅ | Local dev / small single-process deploys |
Both api and worker still expose /health and /metrics (so Kubernetes can probe
and Prometheus can scrape the send/fail counters, which are incremented worker-side).
docker-compose.yml runs api + worker; a bare pnpm start:dev defaults to all.
| Concern | Choice |
|---|---|
| Framework | NestJS |
| ORM / Database | MikroORM + PostgreSQL |
| Queue | BullMQ + Redis |
| Templating | Handlebars |
| Metrics | Prometheus (prom-client) |
- Node.js 22+
- pnpm 9+
- Docker (for the Compose stack)
# 1. Install dependencies
pnpm install
# 2. Configure environment
cp .env.example .env
# edit .env — at minimum set DATABASE_URL, REDIS_URL and API_KEY_HASH
# 3. Generate an API key hash (SHA-256 hex of your key)
node -e "console.log(require('crypto').createHash('sha256').update('your-secret-key').digest('hex'))"
# paste the result into API_KEY_HASH
# 4. Apply database migrations on startup
# set RUN_MIGRATIONS=true in .env (the app runs pending migrations when it boots)
# 5. Start in watch mode
pnpm start:devcp .env.example .env # set POSTGRES_PASSWORD and REDIS_PASSWORD
docker compose up --buildThis starts the API, two workers, PostgreSQL, and Redis.
All endpoints require an X-API-Key header.
Queue a notification.
curl -X POST http://localhost:3000/notify \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{
"channel": "email",
"recipient": "user@example.com",
"template": "welcome-email",
"subject": "Welcome!",
"data": { "name": "John", "appName": "Chapar" }
}'{ "logId": "f7c1e0a2-..." }Query notification history (paginated, filterable by channel, status, from, to).
curl "http://localhost:3000/logs?status=SENT&channel=email&page=1&limit=20" \
-H "X-API-Key: your-secret-key"Unauthenticated liveness/readiness probe.
Prometheus scrape endpoint. Exposes notifications_sent_total,
notifications_failed_total, notification_processing_duration_seconds, and more.
The push channel supports two interchangeable strategies, selected by
FIREBASE_NOTIFICATION:
FIREBASE_NOTIFICATION=true— deliver via Firebase Cloud Messaging (recipientis a device token).FIREBASE_NOTIFICATION=false— publish to Redis Pub/Sub on channelnotifications:<recipient>. Your application subscribes to that channel and renders the notification in-app. Chapar is the publisher only.
Thanks to the strategy pattern, adding a channel (e.g. Telegram) requires no changes to the core pipeline:
- Create a class extending
BaseProviderand implementingsend(). - Register it in
ProvidersModule.
@Injectable()
export class TelegramProvider extends BaseProvider {
readonly channel = 'telegram' as const;
async send(payload: NotificationPayload): Promise<void> {
// call the Telegram Bot API
}
}👉 For the full step-by-step walkthrough (config, factory wiring, migrations, tests), see the provider guide in CONTRIBUTING.md.
The image ships with a few built-in templates, but you can add or override templates
without rebuilding — mount a directory of .hbs files and point TEMPLATES_DIR at it:
# docker-compose
services:
chapar-worker:
image: monasher/chapar:latest
environment:
TEMPLATES_DIR: /templates
volumes:
- ./my-templates:/templates:ro# or plain docker run
docker run -d -v ./my-templates:/templates:ro -e TEMPLATES_DIR=/templates monasher/chaparResolution order: TEMPLATES_DIR first, then the built-ins — so my-templates/welcome-email.hbs
overrides the bundled welcome-email, and any new file (e.g. order-shipped.hbs) becomes
immediately addressable as "template": "order-shipped". File edits are detected via mtime
and picked up on the next render — no restart required. Mount the volume on whichever
service renders: the worker (or the single container when CHAPAR_ROLE=all).
See .env.example for the full list. Configuration is validated with Joi
at startup — the app refuses to boot with missing or invalid values.
Kubernetes manifests live in k8s/ (Deployments, Service, Ingress with TLS,
HPA, PodDisruptionBudget, and a Prometheus ServiceMonitor):
kubectl apply -k k8s/pnpm test # unit tests
pnpm test:e2e # end-to-end testsContributions are welcome — new providers and channels especially! Please read CONTRIBUTING.md for the development workflow, coding standards, commit conventions, and a step-by-step guide to adding a notification provider.
For anything substantial, open an issue first so we can align on the approach.
Mohammad Nasher — nasher.themo@gmail.com