# How Motrix NotificationCenter Delivers System Notifications: Architecture and Implementation

> Discover how Motrix NotificationCenter delivers system notifications using a SQLite database, EventBus IPC, and Electron's native OS notifications. Learn the architecture and implementation.

- Repository: [Dr_rOot/Motrix](https://github.com/agalwood/Motrix)
- Tags: architecture
- Published: 2026-08-19

---

**Motrix's NotificationCenter delivers system notifications by persisting them to a SQLite database in the main process, emitting IPC events through an EventBus, and rendering native OS notifications via Electron's Notification API in the renderer process.**

The Motrix download manager (agalwood/Motrix) implements a robust notification system that bridges the main process and renderer to deliver native system alerts. At the heart of this system lies the NotificationCenter, a core module that manages the complete lifecycle of user-facing notifications—from creation and persistence to cross-process propagation and OS-level display.

## Core Architecture of NotificationCenter

The NotificationCenter operates across three distinct architectural layers that ensure reliable delivery of system notifications even if the application crashes.

### Main Process Layer (Core)

The foundation resides in [`src/core/notifications/notification-center.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/notifications/notification-center.ts). When any component calls `notificationCenter.notify()`, the method performs two critical operations: it writes the notification atomically to the `MotrixDatabase` (SQLite), and it emits `Events.NotificationAdded` and `Events.NotificationsChanged` through the injected emit function (lines 62-78).

This persistence-first approach makes the database the single source of truth. The emitted events carry the fresh database row, allowing reactive updates across the application without querying the database on every change.

### IPC Bridge Layer

The IPC layer in [`src/server/ipc/queries.ts`](https://github.com/agalwood/Motrix/blob/main/src/server/ipc/queries.ts) exposes read operations including `ListNotifications` and `GetUnreadNotificationCount` (lines 248-252), while [`src/server/ipc/commands.ts`](https://github.com/agalwood/Motrix/blob/main/src/server/ipc/commands.ts) handles state mutations like `MarkNotificationRead` (lines 734-743).

Both layers forward core events to every renderer process via a shared **EventBus**. This decouples the notification storage from the UI, ensuring that multiple renderer processes receive real-time updates without direct database access.

### Renderer Layer (UI and OS Bridge)

In the renderer process, two hooks manage the user interface and system integration. The `useNotificationToasts` hook in [`src/renderer/hooks/use-notification-toasts.ts`](https://github.com/agalwood/Motrix/blob/main/src/renderer/hooks/use-notification-toasts.ts) registers a listener on `Events.NotificationAdded` (lines 215-220). When triggered, it resolves localization keys using `resolveNotificationText` and invokes Electron's `new Notification()` constructor, which delegates to the operating system's native notification APIs (macOS Notification Center, Windows Action Center, or Linux libnotify).

Simultaneously, `useNotifications` in [`src/renderer/components/notification-center/use-notifications.ts`](https://github.com/agalwood/Motrix/blob/main/src/renderer/components/notification-center/use-notifications.ts) listens to `Events.NotificationsChanged` (lines 36-48) to synchronize the notification list and unread badge count in the UI.

## Step-by-Step Notification Flow

Understanding the practical flow clarifies how Motrix transforms internal events into visible system alerts:

1. **Trigger**: A core component (e.g., the download engine) calls `notificationCenter.notify()` with parameters including `sourceKey`, `kind`, `severity`, and localization keys defined in [`src/shared/types/notification.ts`](https://github.com/agalwood/Motrix/blob/main/src/shared/types/notification.ts).
2. **Persistence**: The NotificationCenter stores the notification in SQLite and fires `Events.NotificationAdded` followed by `Events.NotificationsChanged`.
3. **Propagation**: The IPC layer relays these events to renderer processes via the shared EventBus defined in [`src/shared/protocol/events.ts`](https://github.com/agalwood/Motrix/blob/main/src/shared/protocol/events.ts).
4. **OS Integration**: The renderer's `useNotificationToasts` receives the event, resolves text via `resolveNotificationText`, and creates an Electron-level `Notification` instance, causing the OS-native notification to appear.
5. **UI Sync**: Concurrently, `useNotifications` receives `Events.NotificationsChanged`, triggering a refresh of the notification pane and unread count badge through `ListNotifications` queries.

Because the core always writes to the database first, notifications survive application restarts. The UI components rely solely on emitted events for real-time feedback, maintaining loose coupling between storage and presentation layers.

## Code Implementation Examples

### Sending Notifications from the Core

When a download fails or completes, core modules invoke the NotificationCenter directly:

```typescript
// In any core module with access to the NotificationCenter
notificationCenter.notify({
  sourceKey: `download-${taskId}`,
  kind: NotificationKinds.EngineFailure,   // Defined in src/shared/types/notification.ts
  severity: 'error',
  titleKey: 'notification.downloadFailed.title',
  titleParams: { name: task.name },
  bodyKey: 'notification.downloadFailed.body',
  taskId,
});

```

This call reaches `NotificationCenter.notify` in [`src/core/notifications/notification-center.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/notifications/notification-center.ts), which stores the row and emits the propagation events.

### Displaying Native OS Toasts

The renderer implements the system notification bridge through a dedicated hook:

```typescript
import { useNotificationToasts } from '@renderer/hooks/use-notification-toasts';

// Inside a top-level component (e.g., App)
function App() {
  useNotificationToasts(); // Registers the listener once
  return <YourAppLayout />;
}

```

Internally, `useNotificationToasts` subscribes to the transport layer:

```typescript
transport.on(Events.NotificationAdded, (row) => {
  const { title, body } = resolveNotificationText(row.key, row.params, i18n);
  new window.Notification(title, { body }); // Electron forwards to OS
});

```

### Querying Notification State

Components access historical notifications and unread counts through the IPC query layer:

```typescript
import { transport } from '@renderer/transport';
import { Queries } from '@shared/protocol/queries';

async function loadNotifications(limit = 50) {
  const items = await transport.invoke(Queries.ListNotifications, limit);
  const unread = await transport.invoke(Queries.GetUnreadNotificationCount);
  // Render list with `items`, display badge using `unread`
}

```

These queries map to `NotificationCenter.list` and `NotificationCenter.unreadCount` in the main process.

## Summary

- **Motrix NotificationCenter** uses a three-layer architecture: core persistence, IPC bridge, and renderer display.
- All notifications are stored in **SQLite** via `MotrixDatabase` before emission, ensuring data survives crashes.
- The **EventBus** in `src/server/ipc/` forwards `NotificationAdded` and `NotificationsChanged` events to renderer processes.
- **Native system notifications** are triggered via Electron's `Notification` API in [`src/renderer/hooks/use-notification-toasts.ts`](https://github.com/agalwood/Motrix/blob/main/src/renderer/hooks/use-notification-toasts.ts).
- The **single source of truth** pattern decouples storage from UI, allowing multiple renderers to stay synchronized through events alone.

## Frequently Asked Questions

### How does Motrix ensure notifications persist after an application crash?

Motrix writes every notification to the SQLite-backed `MotrixDatabase` in [`src/core/notifications/notification-center.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/notifications/notification-center.ts) before emitting any events. This persistence-first approach means that even if the application crashes immediately after creating a notification, the data remains intact and will be available when Motrix restarts. The UI layers only consume emitted events for real-time updates while treating the database as the authoritative source.

### What is the difference between NotificationAdded and NotificationsChanged events?

`Events.NotificationAdded` fires when a single new notification is created, carrying the specific database row for immediate toast display in `useNotificationToasts`. `Events.NotificationsChanged` fires whenever the notification collection mutates, including additions, deletions, or read-state changes. The renderer uses the former for immediate OS-level alerts, and the latter for refreshing list views and unread badges via `useNotifications` in [`src/renderer/components/notification-center/use-notifications.ts`](https://github.com/agalwood/Motrix/blob/main/src/renderer/components/notification-center/use-notifications.ts).

### Can Motrix display notifications when the window is minimized or hidden?

Yes. Because the notification flow originates in the main process and relies on Electron's `window.Notification` API rather than DOM-based notifications, Motrix can display native system notifications even when the renderer window is minimized or hidden. The main process continues to run in the background, persisting notifications to SQLite and emitting IPC events that the renderer processes upon becoming active.

### Where are the notification type definitions located in the codebase?

All notification types, including `NotificationKind`, `NotificationSeverity`, and the notification schema interfaces, are defined in [`src/shared/types/notification.ts`](https://github.com/agalwood/Motrix/blob/main/src/shared/types/notification.ts). These shared types ensure type safety across the main process, IPC layer, and renderer components, preventing drift between how notifications are created in the core and how they are interpreted in the UI.