NotifyHub
MCP serverCommunicationSend notifications across 23 channels with 36 AI-ready tools. One API, zero boilerplate.
Unavailable. This server has no hosted endpoint yet, so ahel can't serve it.
Connect ahel once, and every AI you use reads what you have installed.
From the project's README
As published by gabrielbbaldez/notify-hub in README.md.
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();
Why NotifyHub?
| Problem | Without NotifyHub | With NotifyHub | |
|---|---|---|---|
| JavaMail config, MIME types, Session... | .via(EMAIL) | ||
| 📱 | SMS | Twilio SDK, different API entirely | .via(SMS) |
| Another Twilio setup, prefix logic | .via(WHATSAPP) | ||
| Slack | Webhook HTTP, JSON payload | .via(SLACK) | |
| Telegram | Bot API, HTTP client setup | .via(TELEGRAM) | |
| Discord | Webhook HTTP, JSON payload | .via(DISCORD) | |
| 👥 | Teams | Incoming Webhook, MessageCard JSON | .via(TEAMS) |
| Push | Firebase Admin SDK, credentials... | .via(PUSH) | |
| 🔗 | Webhook | Custom HTTP, payload template | .via(Channel.custom("pagerduty")) |
| WebSocket | Java WebSocket API, reconnect logic | .via(WEBSOCKET) | |
| Google Chat | Webhook HTTP, JSON payload | .via(GOOGLE_CHAT) | |
| Twitter/X | OAuth 1.0a, API v2 setup | .via(TWITTER) | |
| 💼 | OAuth 2.0, REST API setup | .via(LINKEDIN) | |
| Notion | Integration Token, API setup | .via(NOTION) | |
| Twitch | OAuth 2.0, Twitch API setup | .via(TWITCH) | |
| YouTube | YouTube Data API v3 setup | .via(YOUTUBE) | |
| Meta Graph API setup | .via(INSTAGRAM) | ||
| 📧 | SendGrid | SendGrid API, webhook tracking | .via(Channel.custom("sendgrid")) |
| TikTok Shop | HMAC-SHA256, Shop API | .via(TIKTOK_SHOP) | |
| Graph API, Page tokens | .via(FACEBOOK) | ||
| ☁️ | AWS SNS | AWS SDK, credentials, ARN | .via(Channel.custom("aws-sns")) |
| Mailgun | Mailgun API, domain setup | .via(Channel.custom("mailgun")) | |
| PagerDuty | Events API v2, routing key | .via(Channel.custom("pagerduty")) | |
| 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 |
Table of Contents
- Quick Start
- Features
- Fallback Chain
- Multi-Channel Send
- Async Sending
- Retry with Backoff
- Templates (Mustache)
- i18n (Internationalization)
- Attachments
- Priority Levels
- Rate Limiting
- Dead Letter Queue (DLQ)
- Batch Send
- Delivery Tracking
- Scheduled Notifications
- Notification Routing
- Notifiable Interface
- Message Deduplication
- Template Versioning
- Custom Channels
- Event Listeners + Spring Events
- Named Recipients
- Message Queue (RabbitMQ / Kafka)
- Circuit Breaker
- Bulkhead (Concurrency Isolation)
- Multi-Channel Orchestration
- A/B Testing
- Cron Scheduling
- Quiet Hours
- Testing Utilities
- Supported Channels
- Admin Dashboard
- Spring Boot Integration
- Configuration Reference
- Without Spring Boot
- MCP Server (AI Agents)
- Running the Demo
- Architecture
- Maven Central
- Roadmap
- License
Quick Start
1. Add the dependency
<dependency>
<groupId>io.github.gabrielbbaldez</groupId>
<artifactId>notify-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
Need extra channels? Add optional modules:
<!-- 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>
2. Configure in application.yml
notify:
channels:
email:
host: smtp.gmail.com
port: 587
username: ${GMAIL_USER}
password: ${GMAIL_PASS}
from: noreply@myapp.com
from-name: MyApp
tls: true
retry:
max-attempts: 3
strategy: exponential
tracking:
enabled: true
3. Inject and use
@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.
Features
Fallback Chain
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 -> Email
Multi-Channel Send
Send 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();
Async Sending
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!"));
Retry with Backoff
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();
Templates (Mustache)
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.
i18n (Internationalization)
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");
}
}
Attachments
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();
Priority Levels
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.
Rate Limiting
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: 1m
Rate limiting uses a token bucket algorithm. URGENT priority notifications always bypass rate limits.
Dead Letter Queue (DLQ)
Failed notifications (after all retries) are automatically captured in the DLQ:
notify:
tracking:
enabled: true
dlq-enabled: true
View 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 reprocessing
Batch Send
Send notifications to multiple recipients at once:
// By email addresses
notify.toAll(List.of("user1@test.com", "user2@test.com", "user3@test.com"))
.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();
Delivery Tracking
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:00Z
For 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: jpa
Scheduled Notifications
Schedule 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();
Notification Routing
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();
Notifiable Interface
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("user@email.com").via(Channel.EMAIL).content("Hello!").send();
notify.toPhone("+5511999999999").via(Channel.SMS).content("Code: 1234").send();
Message Deduplication
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:
content-hash— SHA-256 hash of recipient + channel + subject + contentexplicit-key— uses the key provided via.deduplicationKey("...")both— uses explicit key if provided, otherwise falls back to content hash
Without Spring Boot:
NotifyHub notify = NotifyHub.builder()
.deduplicationStore(new InMemoryDeduplicationStore(Duration.ofHours(12)))
.channel(emailChannel)
.build();
Template Versioning
Manage multiple versions of templates for A/B testing or gradual rollouts:
templates/notify/
├── order-confirmed.html ← default version
├── order-confirmed@v1.html ← version v1
├── order-confirmed@v2.html ← version v2
├── order-confirmed_pt_BR@v2.html ← 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}
Custom Channels
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.
Event Listeners + Spring Events
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());
}
}
Named Recipients
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/XXX
Resolution order: alias match in recipients map > raw URL/value passthrough > default from config.
Shortened here. Read the whole README on GitHub.
Signals
- GitHub stars
- 5
- Forks
- 1
- Last commit
- Mar 2026
Advanced
- Delivery
- notify-hub MCP server → your ahel gateway (mcp.ahel.ai) → every connected AI client.
- Catalog kind
- mcp-server
- Gateway key
io-github-gabrielbbaldez-notify-hub- Source
- github.com/gabrielbbaldez/notify-hub