# How Macro's Notification Service Delivers and Manages Push and Email Notifications

> Discover how Macro's notification service manages push and email delivery with its Axum-based microservice architecture and polling system, respecting user preferences.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: internals
- Published: 2026-08-18

---

**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`](https://github.com/macro-inc/macro/blob/main/main.rs) file boots the HTTP server, loads environment configuration via [`config.rs`](https://github.com/macro-inc/macro/blob/main/config.rs), registers API routes defined in [`api/mod.rs`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/notification/send/email/mod.rs), which renders templates via [`email/template.rs`](https://github.com/macro-inc/macro/blob/main/email/template.rs) before 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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/config.rs) and [`env.rs`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/main.rs) and injected into the application state for use by sender modules and the poller.

## Integration Examples

**Sending a Push Notification**

```rust
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**

```rust
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`](https://github.com/macro-inc/macro/blob/main/email/mod.rs) handler renders the template with the provided variables and forwards the message to the email microservice.

**Managing Unsubscribe Preferences**

```rust
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`](https://github.com/macro-inc/macro/blob/main/metadata_utils/mod.rs), and dispatch via channel-specific senders.
- **Dual channel support**: Push notifications are handled by [`notification/send/push/mod.rs`](https://github.com/macro-inc/macro/blob/main/notification/send/push/mod.rs) (integrating with SNS/FCM), while emails are processed by [`notification/send/email/mod.rs`](https://github.com/macro-inc/macro/blob/main/notification/send/email/mod.rs) with template rendering.
- **User preferences are enforced** through the unsubscribe API (`/api/v1/notification/unsubscribe/*`), which writes to the `unsubscribes` table checked by the poller before each send attempt.
- **Configuration is type-safe**, leveraging the `macro_env_var` crate in [`config.rs`](https://github.com/macro-inc/macro/blob/main/config.rs) to 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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/main.rs). It uses Axum for HTTP request handling, route registration in [`api/mod.rs`](https://github.com/macro-inc/macro/blob/main/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.