How Macro's Notification Service Handles Push and In-App Notifications

The Macro platform routes user notifications through dedicated transport adapters for push (APNs) and in-app (WebSocket) delivery, unified by a shared domain model and event-driven architecture.

The Macro codebase implements a Notification Service (services/notification_service) that cleanly separates external push delivery from real-time UI updates. This article examines the service's architecture, data flows, and key source files to explain how both notification channels operate.


Architecture Overview

The service follows a layered design with clear separation between domain logic, transport adapters, and event brokers.

Layer Responsibility Key Files
API entry point HTTP routing for CRUD operations services/notification_service/src/main.rs
Domain model Core Notification struct and enums services/notification_service/src/model/notification.rs
Transport adapters Push, email, and in-app delivery modules Push: notification/send/push/mod.rs; Email: notification/send/email/mod.rs
Outbound event broker Publishes NotificationEvent to message bus crates/notification/src/outbound/notification_events.rs
In-App consumer WebSocket forwarding via Tauri extension crates/notification/src/inbound/notification_events_listener.rs
Web UI React components for notification rendering apps/web/src/features/notifications/notification-platform.ts

Both push and in-app flows share the same domain model in model/notification.rs, ensuring consistent data representation while allowing each transport to layer its own metadata.


Push Notification Delivery Flow

1. API Request and Domain Transformation

Client applications POST to /user-notifications (defined in api/user_notification.rs). The service layer transforms this request into an internal notification::domain::models::apple::PushNotificationData struct.

2. Push Adapter Serialization

The notification/send/push/mod.rs adapter converts the internal model into the public PushNotificationData struct:

use notification_service::notification::send::push::PushNotificationData;
use notification_service::notification::domain::models::apple::PushNotificationData as Inner;

// `inner` originates from the Apple-specific domain model
let payload = PushNotificationData::new_from_inner(inner);

// Serialized to JSON and delivered via APNs client
notification_service::notification::send::push::send(payload).await?;

Key fields include notification_id and optional sender_profile_picture_url for rich push styling.

3. External Provider Integration

The APNs client is configured through env.rs/config.rs and delivers to device tokens stored per-user. On successful send, notification_events.rs emits a NotificationDelivered event that the in-app consumer can process for UI synchronization.


In-App Real-Time Notification Flow

1. Event Generation

Any notification creation or status change publishes a NotificationEvent via the outbound broker in crates/notification/src/outbound/notification_events.rs:

use macro_notification::outbound::notification_events::{NotificationEvent, NotificationCreated};

let event = NotificationEvent::Created(NotificationCreated {
    notification_id: uuid::Uuid::new_v4(),
    user_id: user.id,
    title: "New comment".into(),
    body: "Someone mentioned you in a document".into(),
    // Optional UI grouping fields
});

macro_notification::outbound::notification_events::publish(event).await?;

2. Event Consumption and WebSocket Forwarding

The listener in crates/notification/src/inbound/notification_events_listener.rs subscribes to the broker, deserializes events, and forwards compact JSON payloads over WebSocket connections managed by the Notification Service Extension (apps/web/src/lib/tauri/notification.ts).

3. Client-Side Reception

The web UI maintains a persistent socket connection:

import { NotificationService } from '@/lib/tauri/notification';

const ws = NotificationService.connect();
ws.onMessage((msg) => {
  const notification = JSON.parse(msg);
  // Update React context, Redux store, or local state
  addInAppNotification(notification);
});

React components in apps/web/src/features/notifications/ handle rendering and stacking logic via notification-stacking.ts, supporting collapseKey-based grouping for related notifications.


Shared Domain Model and Transport-Specific Extensions

The unified architecture in Macro's Notification Service prevents data drift between channels. The Notification struct in services/notification_service/src/model/notification.rs defines:

  • Type enum: Classification (comment, mention, system)
  • Delivery status: Pending, sent, failed, read
  • Timestamp tracking: Created, updated, delivered, read

Each transport adapter extends this base with channel-specific metadata:

  • Push: sender_profile_picture_url, badge_count, sound settings
  • In-App: collapseKey, priority, expiration for UI behavior

Key Implementation Files

Path Purpose
services/notification_service/src/main.rs Service bootstrap, Axum routing, DI container
services/notification_service/src/model/notification.rs Core domain types and serialization
services/notification_service/src/notification/send/push/mod.rs APNs payload construction and delivery
services/notification_service/src/notification/send/email/mod.rs SMTP/template integration
crates/notification/src/outbound/notification_events.rs Kafka/SQS publishing for NotificationEvent
crates/notification/src/inbound/notification_events_listener.rs Event consumption and WebSocket dispatch
apps/web/src/lib/tauri/notification.ts Tauri WebSocket client for desktop
apps/web/src/features/notifications/notification-platform.ts UI abstraction for notification rendering

Summary

  • Macro's Notification Service centralizes routing logic in services/notification_service while delegating to transport-specific adapters
  • Push notifications flow through notification/send/push/mod.rs to APNs with structured PushNotificationData
  • In-app updates use an event-driven bus: notification_events.rs publishes, notification_events_listener.rs consumes, and Tauri/WebSocket delivers to React
  • Both channels share model/notification.rs for consistent domain representation
  • Async Rust handles backend concurrency; TypeScript/React manages real-time UI state

Frequently Asked Questions

How does Macro ensure in-app notifications appear instantly?

The service uses WebSocket connections managed by a Tauri-based Notification Service Extension. When notification_events_listener.rs receives a NotificationEvent from the message bus, it immediately forwards a JSON payload to all connected clients. The React UI subscribes via notification-source.ts and re-renders without polling.

What message broker does Macro use for notification events?

According to the source code in crates/notification/src/outbound/notification_events.rs, the platform supports Kafka or SQS as configurable backends. The outbound module abstracts the specific provider behind a unified publish() interface, allowing environment-based switching.

Can push and in-app notifications be sent simultaneously?

Yes. The service emits a NotificationDelivered event after successful APNs transmission, which the in-app consumer processes. This ensures UI consistency—users see real-time confirmation that their push notification reached the device. Both transports originate from the same domain model in model/notification.rs.

Where is the Apple Push Notification client configured?

APNs credentials and endpoint settings are loaded from environment variables in services/notification_service/src/config.rs (referenced via env.rs). The push adapter in notification/send/push/mod.rs initializes the client at service startup and reuses it for connection pooling.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →