# Fluxer Notification Architecture: How It Handles Cross-Platform Differences

> Explore Fluxer's three-layer notification architecture. Learn how it seamlessly manages cross-platform differences, integrating native OS APIs for desktop and Web Notifications for browsers and PWAs.

- Repository: [Fluxer/fluxer](https://github.com/fluxerapp/fluxer)
- Tags: architecture
- Published: 2026-03-17

---

**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`](https://github.com/fluxerapp/fluxer/blob/main/NotificationStore.tsx)): Decides when and what to notify, tracks shown notifications, and manages user preferences
- **Platform-Abstraction Utilities** ([`NotificationUtils.tsx`](https://github.com/fluxerapp/fluxer/blob/main/NotificationUtils.tsx)): Provides a unified `showNotification()` API that selects the best transport for the current runtime
- **Runtime Bridges** ([`preload/index.tsx`](https://github.com/fluxerapp/fluxer/blob/main/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`](https://github.com/fluxerapp/fluxer/blob/main/src/stores/NotificationStore.tsx), the constructor handles persistence and permission refresh:

```typescript
// 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`](https://github.com/fluxerapp/fluxer/blob/main/NotificationStore.tsx) (lines 302-398) handles title construction, sound decisions, and tracking:

```typescript
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`](https://github.com/fluxerapp/fluxer/blob/main/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:

```typescript
// 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`](https://github.com/fluxerapp/fluxer/blob/main/src/preload/index.tsx) exposes a safe API to the renderer process via IPC:

```typescript
// 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()` returns `true`, so `requestPermission()` immediately dispatches a granted action and shows a confirmation toast, as the native OS manages permissions
- **Browser**: Calls `Notification.requestPermission()` and updates `browserNotificationsEnabled` based on the user response, then registers push subscriptions if granted and running as an installed PWA

```typescript
// 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`](https://github.com/fluxerapp/fluxer/blob/main/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.