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

> Discover how Macro's notification service uses transport adapters for APNs push and WebSocket in-app notifications, powered by a unified domain model and event-driven architecture.

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

---

**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](https://github.com/macro-inc/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`](https://github.com/macro-inc/macro/blob/main/services/notification_service/src/main.rs) |
| **Domain model** | Core `Notification` struct and enums | [`services/notification_service/src/model/notification.rs`](https://github.com/macro-inc/macro/blob/main/services/notification_service/src/model/notification.rs) |
| **Transport adapters** | Push, email, and in-app delivery modules | Push: [`notification/send/push/mod.rs`](https://github.com/macro-inc/macro/blob/main/notification/send/push/mod.rs); Email: [`notification/send/email/mod.rs`](https://github.com/macro-inc/macro/blob/main/notification/send/email/mod.rs) |
| **Outbound event broker** | Publishes `NotificationEvent` to message bus | [`crates/notification/src/outbound/notification_events.rs`](https://github.com/macro-inc/macro/blob/main/crates/notification/src/outbound/notification_events.rs) |
| **In-App consumer** | WebSocket forwarding via Tauri extension | [`crates/notification/src/inbound/notification_events_listener.rs`](https://github.com/macro-inc/macro/blob/main/crates/notification/src/inbound/notification_events_listener.rs) |
| **Web UI** | React components for notification rendering | [`apps/web/src/features/notifications/notification-platform.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/features/notifications/notification-platform.ts) |

Both push and in-app flows share the same **domain model** in [`model/notification.rs`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/notification/send/push/mod.rs) adapter converts the internal model into the public `PushNotificationData` struct:

```rust
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`](https://github.com/macro-inc/macro/blob/main/env.rs)/[`config.rs`](https://github.com/macro-inc/macro/blob/main/config.rs) and delivers to device tokens stored per-user. On successful send, [`notification_events.rs`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/crates/notification/src/outbound/notification_events.rs):

```rust
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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/tauri/notification.ts)).

### 3. Client-Side Reception

The web UI maintains a persistent socket connection:

```typescript
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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/services/notification_service/src/main.rs) | Service bootstrap, Axum routing, DI container |
| [`services/notification_service/src/model/notification.rs`](https://github.com/macro-inc/macro/blob/main/services/notification_service/src/model/notification.rs) | Core domain types and serialization |
| [`services/notification_service/src/notification/send/push/mod.rs`](https://github.com/macro-inc/macro/blob/main/services/notification_service/src/notification/send/push/mod.rs) | APNs payload construction and delivery |
| [`services/notification_service/src/notification/send/email/mod.rs`](https://github.com/macro-inc/macro/blob/main/services/notification_service/src/notification/send/email/mod.rs) | SMTP/template integration |
| [`crates/notification/src/outbound/notification_events.rs`](https://github.com/macro-inc/macro/blob/main/crates/notification/src/outbound/notification_events.rs) | Kafka/SQS publishing for `NotificationEvent` |
| [`crates/notification/src/inbound/notification_events_listener.rs`](https://github.com/macro-inc/macro/blob/main/crates/notification/src/inbound/notification_events_listener.rs) | Event consumption and WebSocket dispatch |
| [`apps/web/src/lib/tauri/notification.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/tauri/notification.ts) | Tauri WebSocket client for desktop |
| [`apps/web/src/features/notifications/notification-platform.ts`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/notification/send/push/mod.rs) to APNs with structured `PushNotificationData`
- **In-app updates** use an event-driven bus: [`notification_events.rs`](https://github.com/macro-inc/macro/blob/main/notification_events.rs) publishes, [`notification_events_listener.rs`](https://github.com/macro-inc/macro/blob/main/notification_events_listener.rs) consumes, and Tauri/WebSocket delivers to React
- **Both channels share [`model/notification.rs`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/services/notification_service/src/config.rs) (referenced via [`env.rs`](https://github.com/macro-inc/macro/blob/main/env.rs)). The push adapter in [`notification/send/push/mod.rs`](https://github.com/macro-inc/macro/blob/main/notification/send/push/mod.rs) initializes the client at service startup and reuses it for connection pooling.