NotifyEngine

ML-powered notification delivery with adaptive channel routing.

The router learns each recipient's delivery preferences per context (which channel works, at what hour, on which day, for what kind of content) instead of following a static preference table. Every send is scored by an XGBoost model over 19 features, including time-of-day and day-of-week patterns, engagement recency, and per-recipient delivery history.

02:00 · night active model · ε = 0.1
enqueue
email 0.81 delivered
websocket 0.34 exploration
sms mock 0.17 exploration
webhook planned · not scored
Illustrative demo cycle: scores are simulated for display. In the real pipeline, each probability comes from the XGBoost model's 19-feature vector; amber is the model's pick, pink is an ε-greedy exploration route.
Pipeline

How it works

Four stages from API call to delivered notification.

01

Enqueue

Validated, deduped on an idempotency key, and queued into one of four priority queues on BullMQ + Redis.

POST /v1/notifications → 202
02

Feature extraction

Per candidate channel: time of day and day of week, engagement recency, per-recipient history, channel health, LLM content signals.

19-feature vector
03

Scoring

Every eligible channel gets an engagement probability; ε-greedy exploration occasionally routes elsewhere so the model keeps learning.

XGBoost · ε = 0.1
04

Delivery with failover

Attempts walk the ranked channels, skip open circuit breakers, retry with backoff. If the ML service is slow, static routing takes over.

2s timeout · DLQ
Reliability

When things fail

What happens when a channel, a provider, or the model itself fails.

Priority queues

Four queues (critical, high, standard, bulk), each with its own retry policy: from 5 attempts at a 1s base delay down to 2 attempts at 30s, all exponential backoff, plus a dead-letter queue for exhausted jobs.

Graceful ML degradation

Hard 2-second prediction timeout; any failure falls back to static priority routing.

Circuit breakers

Per channel: a 60-second rolling window opens the breaker above a 50% failure rate (minimum 10 samples); half-open probes with escalating cooldown recover it while traffic reroutes to the next-best channel.

Postgres RLS

Row-level security enabled and forced: tenant isolation below the application layer.

Hashed API keys

SHA-256 stored, constant-time compared, revocable and expiring; raw keys shown once.

Rate limiting

Per-tenant atomic Redis counters; X-RateLimit-* headers and proper 429 + Retry-After.

Idempotency keys

Retried requests replay the original response instead of enqueuing duplicates.

Full reliability spec
  • Four priority queues (critical / high / standard / bulk) with per-priority retry policies (from 5 attempts at a 1s base delay for critical down to 2 attempts at 30s for bulk), all exponential backoff, plus a dead-letter queue for exhausted jobs.
  • Per-channel circuit breakers. A 60-second rolling failure window opens the breaker above a 50% failure rate (minimum 10 samples). Recovery uses half-open probe attempts with an escalating cooldown (60s per consecutive failure, capped at 300s). While a breaker is open, traffic reroutes to the next-best channel automatically.
  • Graceful ML degradation. Predictions have a hard 2-second timeout; any failure falls back to static priority routing.
  • Multi-tenancy in the database. Postgres row-level security is enabled and forced on tenant tables, with tenant context set per connection. Isolation is enforced below the application layer, not just in query code.
  • API keys stored as SHA-256 hashes with constant-time comparison, revocation, and expiry. Raw keys are shown once at creation and never persisted.
  • Per-tenant rate limiting via an atomic Redis script, with X-RateLimit-* headers on every response and proper 429 + Retry-After semantics on breach.
  • Idempotency keys. Retried requests replay the original response instead of enqueuing duplicates.
Training loop

Synthetic engagement harness

Adaptive routing needs labeled engagement data, which a new deployment doesn't have.

  1. simulate
  2. label
  3. retrain · 6h
  4. gate · AUC
  5. promote

LLM-driven recipient personas (email_lover, push_fan, disengaged) decide whether each delivered notification would be engaged with, and a cold-start model is bootstrapped from synthetic recipient archetypes with realistic time-of-day behavior.

On that data the full training loop runs for real: automatic retraining every 6 hours on labeled delivery attempts, an AUC-gated promotion step with an overfit guard and a minimum sample floor, and a versioned model registry with per-model metrics and feature importance. All of it is triggerable on demand from the dashboard, before any production traffic exists.

Engagement in the demo environment is simulated, stored explicitly as simulated engagement. The one genuine engagement signal today is the email open-tracking pixel.

Delivery

Channels

New channels plug into a registry behind a single deliver() interface.

Channel Status Detail
Email Live Real SMTP delivery (nodemailer); Mailpit as the dev sink.
WebSocket Live Presence-aware real-time delivery over Socket.IO via Redis pub/sub.
SMS Mocked Demo mock only. No provider integration yet.
Webhook Planned Reserved in the schema and types; delivery not implemented.
Under the hood

Stack

Express / TypeScript · FastAPI / XGBoost · Postgres 16 · Redis 7 + BullMQ · React 19 · Socket.IO · Turborepo

Source

Read the source

Every claim on this page is traceable to the codebase.