How Motrix NotificationCenter Delivers System Notifications: Architecture and Implementation
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. 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 exposes read operations including ListNotifications and GetUnreadNotificationCount (lines 248-252), while 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 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 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:
- Trigger: A core component (e.g., the download engine) calls
notificationCenter.notify()with parameters includingsourceKey,kind,severity, and localization keys defined insrc/shared/types/notification.ts. - Persistence: The NotificationCenter stores the notification in SQLite and fires
Events.NotificationAddedfollowed byEvents.NotificationsChanged. - Propagation: The IPC layer relays these events to renderer processes via the shared EventBus defined in
src/shared/protocol/events.ts. - OS Integration: The renderer's
useNotificationToastsreceives the event, resolves text viaresolveNotificationText, and creates an Electron-levelNotificationinstance, causing the OS-native notification to appear. - UI Sync: Concurrently,
useNotificationsreceivesEvents.NotificationsChanged, triggering a refresh of the notification pane and unread count badge throughListNotificationsqueries.
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:
// 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, which stores the row and emits the propagation events.
Displaying Native OS Toasts
The renderer implements the system notification bridge through a dedicated hook:
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:
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:
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
MotrixDatabasebefore emission, ensuring data survives crashes. - The EventBus in
src/server/ipc/forwardsNotificationAddedandNotificationsChangedevents to renderer processes. - Native system notifications are triggered via Electron's
NotificationAPI insrc/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 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.
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. 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.
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 →