How Macro's Notification Service Delivers and Manages Push and Email Notifications
Macro's notification service is a standalone Axum-based microservice that orchestrates push and email delivery through a polling-based architecture, utilizing separate sender modules for each channel while respecting user unsubscribe preferences stored in a dedicated database table.
The notification service in the macro-inc/macro repository provides a centralized pipeline for delivering user alerts across multiple channels. Implemented as a standalone microservice in services/notification_service, it exposes REST endpoints for notification management while using background pollers to handle actual delivery through external providers like Amazon SNS, Firebase, or internal email services.
Architecture Overview
The service follows a modular Rust architecture centered around Axum for HTTP handling and structured domain separation.
Entry Point and Server Setup
The main.rs file boots the HTTP server, loads environment configuration via config.rs, registers API routes defined in api/mod.rs, and initializes background pollers. All routes are mounted under the /api/v1/notification prefix, providing a consistent namespace for notification operations.
Core Data Model
The Notification struct defined in model/notification.rs serves as the domain model throughout the service. It captures the user_id, channel type (push or email), JSON payload, delivery status, and timestamps. This model is used by both the API layer for ingestion and the sender modules for dispatch.
Background Processing
Rather than processing notifications synchronously, the service relies on poller logic (implemented in poller_tests.rs) to periodically scan the database for rows with status = queued. This decouples the API request from the delivery attempt, allowing for retries and resilience against external provider outages.
Notification Delivery Pipeline
The delivery flow follows a five-stage pipeline that ensures reliable message dispatch while respecting user preferences.
1. Creation and Persistence
Upstream services (such as document_storage_service or email_service) create notifications by writing rows to the notifications table. Each row specifies the target user, desired channel, and a JSON payload containing message content.
2. Polling and Metadata Enrichment
The background poller retrieves pending notifications and invokes metadata_utils/mod.rs to gather user-specific context. This includes locale settings, preferred channels, and do-not-disturb windows. The metadata is merged into the payload before dispatch.
3. Channel Dispatch
Based on the channel field, the poller routes the notification to the appropriate sender implementation:
- Push notifications are handled by
notification/send/push/mod.rs, which serializes the payload for external providers like Amazon SNS or Firebase Cloud Messaging. - Email notifications are processed by
notification/send/email/mod.rs, which renders templates viaemail/template.rsbefore forwarding to the email delivery microservice.
4. External Delivery
The push sender constructs provider-specific payloads and executes network calls (e.g., sns.publish or FCM HTTP requests). The email sender renders HTML templates using the supplied template_vars and delegates to an internal HTTP client.
5. Status Management
After the external call completes, the poller updates the notification row. Successful deliveries are marked sent, while failures are recorded as failed with error codes to enable retry logic or dead-letter analysis.
Channel-Specific Implementations
Push Notification Sender
Located in notification/send/push/mod.rs, the push sender converts the internal Notification payload into provider-specific formats. It supports multiple backend providers through configuration, allowing the service to switch between Amazon SNS, Firebase, or custom endpoints without changing the API contract. The module handles payload construction, authentication with provider APIs, and response parsing to determine delivery success.
Email Notification Sender
The email pipeline in notification/send/email/mod.rs handles template-based message generation. It consumes the JSON payload from the notification row, extracts template_vars, and renders the final HTML using the rendering logic in email/template.rs. The rendered message is then forwarded to a dedicated email delivery microservice via an internal HTTP client, separating transport concerns from content generation.
User Subscription Management
Users control their notification experience through a dedicated unsubscribe API defined in api/unsubscribe/.
Opt-Out Endpoints
The service exposes three REST endpoints for preference management:
POST /api/v1/notification/unsubscribe/email– Disables email delivery for the specified user.POST /api/v1/notification/unsubscribe/push– Disables push delivery.POST /api/v1/notification/unsubscribe/all– Disables every channel.
Each endpoint writes a record to the unsubscribes table. The poller checks this table before invoking any sender, ensuring that user preferences are respected even if notifications are already queued.
Configuration and Environment Security
Service configuration is centralized in config.rs and env.rs, which utilize the macro_env_var crate to read settings. This approach ensures that sensitive data such as push provider credentials, email SMTP details, and rate-limiting parameters are parsed through a typed interface rather than raw std::env::var calls. The configuration is loaded at startup in main.rs and injected into the application state for use by sender modules and the poller.
Integration Examples
Sending a Push Notification
use macro_notification_service::client::NotificationClient;
use macro_notification_service::model::Notification;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let client = NotificationClient::new("http://localhost:8080")?;
let notif = Notification {
user_id: "user-123".into(),
channel: "push".into(),
payload: serde_json::json!({
"title": "New Document",
"body": "A document has been shared with you."
}),
..Default::default()
};
client.send_notification(notif).await?;
Ok(())
}
The client posts to /api/v1/notification/user/{user_id}, persisting the notification for poller pickup.
Sending an Email Notification
let email_notif = Notification {
user_id: "user-456".into(),
channel: "email".into(),
payload: serde_json::json!({
"subject": "Welcome to Macro!",
"template_vars": { "first_name": "Alice" }
}),
..Default::default()
};
client.send_notification(email_notif).await?;
The email/mod.rs handler renders the template with the provided variables and forwards the message to the email microservice.
Managing Unsubscribe Preferences
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let http = Client::new();
http.post("http://localhost:8080/api/v1/notification/unsubscribe/all")
.json(&serde_json::json!({ "user_id": "user-123" }))
.send()
.await?;
Ok(())
}
This updates the unsubscribes table, preventing future deliveries to the specified user.
Summary
- Macro's notification service operates as a standalone Axum microservice in
services/notification_service, providing REST APIs for notification creation and subscription management. - Delivery is asynchronous: Background pollers scan for queued notifications, enrich them with user metadata from
metadata_utils/mod.rs, and dispatch via channel-specific senders. - Dual channel support: Push notifications are handled by
notification/send/push/mod.rs(integrating with SNS/FCM), while emails are processed bynotification/send/email/mod.rswith template rendering. - User preferences are enforced through the unsubscribe API (
/api/v1/notification/unsubscribe/*), which writes to theunsubscribestable checked by the poller before each send attempt. - Configuration is type-safe, leveraging the
macro_env_varcrate inconfig.rsto manage provider credentials without exposing raw environment variables.
Frequently Asked Questions
How does Macro's notification service handle delivery failures?
The background poller marks failed deliveries with a failed status and associated error codes in the database. This design separates failure recording from the delivery attempt itself, allowing operators to implement retry logic, dead-letter queues, or alerting based on the persisted error state without blocking the main processing loop.
What channels does the Macro notification service support?
According to the source code in model/notification.rs and the sender implementations, the service supports push notifications (delivered via Amazon SNS or Firebase) and email notifications (rendered through templates and forwarded to an internal email service). The architecture allows for additional channels by implementing new sender modules in notification/send/.
How can users unsubscribe from Macro notifications?
Users can opt out through three dedicated REST endpoints: POST /api/v1/notification/unsubscribe/email, POST /api/v1/notification/unsubscribe/push, and POST /api/v1/notification/unsubscribe/all. Each endpoint updates the unsubscribes table, which the poller checks before invoking any sender to ensure preferences are respected.
What framework powers Macro's notification service?
The service is built on the Axum web framework for Rust, as implemented in main.rs. It uses Axum for HTTP request handling, route registration in api/mod.rs, and state management for configuration and database connections. The async runtime is provided by Tokio, enabling the concurrent poller and API server operations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →