Fluxer Notification Architecture: How It Handles Cross-Platform Differences
Fluxer implements a three-layer notification architecture that cleanly separates business logic from platform-specific transport, automatically routing to Electron's native OS APIs on desktop while falling back to Service Workers and Web Notification APIs in browsers and PWAs.
The notification system in the fluxerapp/fluxer repository demonstrates how modern TypeScript applications can deliver consistent user experiences across disparate runtimes. By abstracting the notification architecture into distinct layers for orchestration, transport, and runtime bridging, Fluxer delivers native desktop notifications via Electron and progressive web app (PWA) notifications through standard browser APIs without duplicating business logic.
The Three-Layer Notification Architecture
Fluxer organizes its notification system into three distinct layers that isolate concerns:
- Store & Orchestration (
NotificationStore.tsx): Decides when and what to notify, tracks shown notifications, and manages user preferences - Platform-Abstraction Utilities (
NotificationUtils.tsx): Provides a unifiedshowNotification()API that selects the best transport for the current runtime - Runtime Bridges (
preload/index.tsx): Concrete implementations for Electron and browser environments
This design ensures that business logic in the store remains agnostic of whether the app runs as a desktop client or web application.
Layer 1: NotificationStore – The Decision Engine
The NotificationStore serves as the central state machine that determines when users receive alerts. It manages observable state, persists preferences, and wires reactions that synchronize push subscriptions with permission changes.
State Management and Persistence
The store initializes with makeAutoObservable and sets up automatic reactions to account changes. According to the source in src/stores/NotificationStore.tsx, the constructor handles persistence and permission refresh:
// NotificationStore constructor (excerpt)
// https://github.com/fluxerapp/fluxer/blob/refactor/fluxer_app/src/stores/NotificationStore.tsx#L30-L48
constructor() {
makeAutoObservable(this, { notifiedMessageIds: false }, { autoBind: true });
this.initPersistence();
queueMicrotask(() => this.refreshPermission());
queueMicrotask(() => NotificationUtils.ensureDesktopNotificationClickHandler());
// Reaction to account changes → (re)register push-subscriptions
this.accountReactionDisposer = reaction(() => AccountManager?.currentUserId,
() => {
if (!shouldManagePushSubscriptions()) return;
if (!this.browserNotificationsEnabled) return;
void PushSubscriptionService.registerPushSubscription();
});
}
Core Notification Flow
When a new message arrives, the store builds the notification payload and delegates display to NotificationUtils. The showNotification() method in NotificationStore.tsx (lines 302-398) handles title construction, sound decisions, and tracking:
private async showNotification(data: NotificationData): Promise<void> {
// …build title & body…
const result = await NotificationUtils.showNotification({
title,
body,
icon: AvatarUtils.getUserAvatarURL(user),
url: notificationUrl,
playSound: false, // sound already played earlier if needed
});
// keep track of the native/browser handle for later cleanup
notificationTracker.track(channel.id, {
browserNotification: result.browserNotification,
nativeId: result.nativeNotificationId,
});
this.markNotified(message.id);
}
The store also respects focus state and channel selection to suppress duplicate alerts, clears tracked notifications on window focus or message acknowledgment, and updates badge counts via unreadMessageBadgeEnabled.
Push Subscription Management
Push subscriptions are managed reactively, but only activate when the app runs as an installed PWA. The store checks isInstalledPwa() before registering with PushSubscriptionService, ensuring web-push behavior aligns with PWA installation status.
Layer 2: NotificationUtils – Platform Abstraction
NotificationUtils.showNotification() acts as the single entry point for all notification requests. Located in src/utils/NotificationUtils.tsx, this function implements a priority-based routing system that detects the runtime environment and selects the appropriate transport mechanism.
Runtime Detection and Routing
The utility checks platforms in strict order: Electron first, then Service Worker, then native browser API:
// NotificationUtils.showNotification – platform decision (excerpt)
// https://github.com/fluxerapp/fluxer/blob/refactor/fluxer_app/src/utils/NotificationUtils.tsx#L19-L48
const electronApi = getElectronAPI();
if (electronApi) {
// Electron path – native OS notification
const result = await electronApi.showNotification({title, body, icon: icon ?? '', url});
return {browserNotification: null, nativeNotificationId: result.id};
}
// Service-worker attempt
const swAttempt = await tryShowNotificationViaServiceWorker({title, body, url, icon, targetUserId});
if (swAttempt.shown) return swAttempt.result;
// Browser Notification fallback
if (typeof Notification !== 'undefined' && Notification.permission === 'granted') {
return tryShowNotificationViaWindowNotification({title, body, url, icon});
}
The isDesktop() helper determines Electron availability, while getElectronAPI() returns the exposed preload interface or null in browser contexts.
Service Worker and Browser Fallbacks
When Electron is unavailable, tryShowNotificationViaServiceWorker() attempts to use the Service Worker registration for PWA-compatible notifications. If that fails or no registration exists, the code falls back to tryShowNotificationViaWindowNotification(), which instantiates the standard Notification constructor and attaches click handlers for window focusing and navigation.
Sound and Cleanup Handling
Sound playback occurs before platform branching via playNotificationSoundIfEnabled(), ensuring consistent audio cues across all runtimes. For cleanup, the store tracks notification IDs returned by Electron or browser instances, enabling targeted dismissal through closeNativeNotification() and closeNativeNotifications() helpers (which noop in browser environments).
Layer 3: Runtime Bridges – Electron and Browser
The final layer provides concrete implementations for each target environment.
Electron Preload Bridge
In the desktop client, src/preload/index.tsx exposes a safe API to the renderer process via IPC:
// preload.showNotification & click listener (excerpt)
// https://github.com/fluxerapp/fluxer/blob/refactor/fluxer_desktop/src/preload/index.tsx#L71-L84
showNotification: (options: NotificationOptions): Promise<NotificationResult> =>
ipcRenderer.invoke('show-notification', options),
onNotificationClick: (callback: (id: string, url?: string) => void): (() => void) => {
const handler = (_event, id, url) => callback(id, url);
ipcRenderer.on('notification-click', handler);
return () => ipcRenderer.removeListener('notification-click', handler);
},
The main process handles the IPC invocation by creating Electron's Notification instance and returning an ID, enabling the store to close native notifications later.
Browser and PWA Implementation
When getElectronAPI() returns null, the system relies on Service Worker registration for installed PWAs. The tryShowNotificationViaServiceWorker() function builds NotificationOptions with url or target_user_id attached to options.data, then calls registration.showNotification(title, options). For standard browsers without Service Worker support, it falls back to the Web Notification API with manual click handling.
Platform-Specific Permission Handling
Fluxer handles permission requests differently based on the runtime:
- Desktop:
isDesktop()returnstrue, sorequestPermission()immediately dispatches a granted action and shows a confirmation toast, as the native OS manages permissions - Browser: Calls
Notification.requestPermission()and updatesbrowserNotificationsEnabledbased on the user response, then registers push subscriptions if granted and running as an installed PWA
// requestPermission – desktop shortcut (excerpt)
// https://github.com/fluxerapp/fluxer/blob/refactor/fluxer_app/src/utils/NotificationUtils.tsx#L89-L100
if (isDesktop()) {
NotificationActionCreators.permissionGranted();
playNotificationSoundIfEnabled();
void showNotification({ title: i18n._(msg`Access granted`), ... });
return;
}
Summary
Fluxer's notification architecture achieves cross-platform consistency through clear separation of concerns:
- NotificationStore manages business logic, timing, and state without platform awareness
- NotificationUtils provides a unified API that routes to Electron, Service Worker, or Web Notification APIs based on runtime detection
- Runtime Bridges implement environment-specific transport mechanisms via IPC for desktop and standard web APIs for browsers
- Permission handling adapts to each platform's requirements, implicitly granting on desktop while explicitly requesting on web
- Push subscriptions activate only for installed PWAs, preventing unnecessary web-push registration in standard browser tabs
Frequently Asked Questions
How does Fluxer decide which notification API to use?
Fluxer checks the runtime environment in a specific priority order. First, it detects Electron via isDesktop() and getElectronAPI(), routing to native OS notifications if available. If not, it attempts Service Worker registration for PWA support, then falls back to the standard browser Notification constructor. This logic is centralized in NotificationUtils.tsx, ensuring components call a single showNotification() method regardless of platform.
What happens when a user clicks a notification in Fluxer?
Click handling depends on the runtime. In Electron, the preload script sets up ipcRenderer.on('notification-click') listeners that invoke callbacks with notification IDs and URLs. In browsers, tryShowNotificationViaWindowNotification() attaches click handlers directly to Notification instances, focusing the window and navigating to the supplied URL. The store tracks these interactions to clear notifications appropriately.
How does Fluxer handle notification permissions differently on desktop vs web?
On desktop (Electron), Fluxer assumes permissions are granted at the OS level, so requestPermission() immediately dispatches a granted action and displays a confirmation toast. On web platforms, it calls the standard Notification.requestPermission() API and updates internal state based on the user's response. Push subscriptions are only registered when permissions are granted and the app runs as an installed PWA.
Can Fluxer notifications work offline?
Yes, but only in specific configurations. When running as an installed PWA with Service Worker support, notifications can display offline because the Service Worker handles the notification event. However, the initial permission check and push subscription registration require connectivity. Desktop Electron notifications work offline natively once the app is running, as they rely on the operating system's notification system rather than web-push infrastructure.
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 →