How Plane's Notification System Handles Real-Time Alerts

Plane delivers real-time notification alerts through a reactive MobX store combined with REST API endpoints, eliminating the need for persistent WebSocket connections while maintaining instant UI updates.

Plane's notification system balances server-side state management with client-side reactivity to create a responsive user experience. The system centers on the WorkspaceNotificationService for API communication and the WorkspaceNotificationStore for state management, working together to keep the notification panel synchronized. According to the makeplane/plane source code, this approach ensures users see immediate visual feedback when interacting with alerts, even though the underlying transport uses standard HTTP requests rather than persistent socket connections.

The WorkspaceNotificationService API Layer

The notification service acts as the central HTTP client for all alert-related operations. Defined in [packages/services/src/workspace/notification.service.ts](https://github.com/makeplane/plane/blob/preview/packages/services/src/workspace/notification.service.ts), this service exposes methods that map to specific REST endpoints for fetching, updating, and managing notification states.

Available Endpoints and Methods

The service implements a complete CRUD interface for notification management:

  • getUnreadCount – Calls GET /api/workspaces/:workspaceSlug/users/notifications/unread/ to retrieve total and mention-specific unread counts
  • list – Fetches paginated notifications via GET /api/workspaces/:workspaceSlug/users/notifications with support for filtering and snoozed/archived flags
  • update – Sends PATCH requests to modify notification properties like snooze status
  • markAsRead – Posts to …/notifications/:id/read/ to mark individual alerts as read
  • markAsUnread – Deletes the read status via DELETE …/notifications/:id/read/
  • archive and unarchive – Manage archived state through POST and DELETE requests to the archive endpoint
  • markAllAsRead – Bulk updates all filtered notifications via POST …/notifications/mark-all-read/

The MobX Store: Reactive State Management

The WorkspaceNotificationStore located at [apps/web/core/store/notifications/workspace-notifications.store.ts](https://github.com/makeplane/plane/blob/preview/apps/web/core/store/notifications/workspace-notifications.store.ts) implements the client-side state container using MobX observables. This store maintains the single source of truth for notification data and automatically triggers React re-renders when state changes.

Fetching and Storing Notifications

When the application needs to display alerts, the store's getNotifications method invokes the service's list endpoint. Upon receiving a TNotificationPaginatedInfo payload, the store extracts the results array and processes each item through mutateNotifications. This creates or updates Notification model instances defined in [apps/web/core/store/notifications/notification.ts](https://github.com/makeplane/plane/blob/preview/apps/web/core/store/notifications/notification.ts).

// Instantiate the service
const notificationService = new WorkspaceNotificationService();

// Fetch the first page of notifications
await workspaceNotificationStore.getNotifications('my-workspace-slug');

Handling Unread Counts

The store maintains unread notification counters through the getUnreadNotificationsCount method. This calls the service's getUnreadCount endpoint and stores the result in the observable unreadNotificationsCount property, enabling real-time badge updates in the UI.

// Get unread counts for the current workspace
const unread = await notificationService.getUnreadCount('my-workspace-slug');

Local State Mutations

When users interact with notifications, the store applies optimistic updates. For example, marking a notification as read immediately calls notification.mutateNotification({ read_at: new Date().toUTCString() }) on the local model instance before or after the API call completes. This ensures the UI reflects changes instantly without waiting for server confirmation or full page refreshes.

// Mark a single notification as read with immediate UI feedback
await notificationService
  .markAsRead('my-workspace-slug', 'notification-id-123')
  .then((updated) => {
    console.log('Marked read:', updated);
  });

Real-Time UI Updates Without WebSockets

Unlike traditional real-time systems that rely on WebSocket connections, Plane achieves reactive updates through a combination of fast API calls and MobX's observable state system. When backend processes—such as the task defined in [apps/api/plane/bgtasks/notification_task.py](https://github.com/makeplane/plane/blob/preview/apps/api/plane/bgtasks/notification_task.py)—create new notification records, the client fetches fresh data during the next panel interaction or manual refresh.

The reactive chain works as follows:

  1. User Action triggers a service method (e.g., markAsRead)
  2. API Response confirms the server state change
  3. Store Mutation updates the MobX observable (e.g., notification.mutateNotification)
  4. Computed Properties like notificationIdsByWorkspaceId and notificationLiteByNotificationId recalculate automatically
  5. React Re-render occurs because components wrapped in observer() from mobx-react-lite detect observable changes

This architecture provides a "real-time-like" experience where the UI appears instantly synchronized with server state.

Implementation Examples

Consuming Notifications in React Components

Components access the store through the useRootStore hook (or the specific hook from [apps/web/core/hooks/store/notifications/use-workspace-notifications.ts](https://github.com/makeplane/plane/blob/preview/apps/web/core/hooks/store/notifications/use-workspace-notifications.ts)) and automatically re-render when notification data changes. The [notification-app-sidebar-option.tsx](https://github.com/makeplane/plane/blob/preview/apps/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx) component demonstrates this pattern by rendering individual notification entries that respond to store updates.

import { observer } from 'mobx-react-lite';
import { useRootStore } from '@/hooks/store-hooks';

const NotificationList = observer(() => {
  const { notification } = useRootStore();
  const ids = notification.notificationIdsByWorkspaceId('workspace-id') ?? [];

  return (
    <ul>
      {ids.map((id) => (
        <li key={id}>{notification.notifications[id].title}</li>
      ))}
    </ul>
  );
});

Bulk Operations

The store supports batch operations that minimize API calls while maintaining state consistency:

// Bulk-mark all filtered notifications as read
await workspaceNotificationStore.markAllNotificationsAsRead('my-workspace-slug');

Individual Notification Model

Each notification wraps its data in a model class that provides type-safe mutation methods:

// Inside workspace-notifications.store.ts
notification.mutateNotification({ 
  read_at: new Date().toUTCString(),
  archived_at: null 
});

Summary

  • Plane's notification system combines REST APIs with MobX reactivity to deliver real-time UI updates without WebSocket dependencies.
  • WorkspaceNotificationService handles all HTTP communication to endpoints for fetching, updating, and bulk-managing notifications.
  • WorkspaceNotificationStore maintains observable state using MobX, with computed properties like notificationIdsByWorkspaceId providing filtered data to components.
  • Optimistic updates occur through the Notification model's mutateNotification method, ensuring immediate visual feedback when users mark items read, unread, or archived.
  • React components consume the store via observer() HOCs from mobx-react-lite, enabling automatic re-renders when notification state changes.

Frequently Asked Questions

Does Plane use WebSockets for real-time notification alerts?

No, Plane does not currently use persistent WebSocket connections for notifications. Instead, the system relies on fast REST API calls combined with MobX's reactive state management to provide immediate UI updates. When the backend creates new notifications via background tasks like notification_task.py, the client fetches fresh data on the next user interaction or manual refresh.

How does Plane keep the unread notification count synchronized?

The WorkspaceNotificationStore maintains an unreadNotificationsCount observable that updates whenever getUnreadNotificationsCount is called. This method queries the GET /api/workspaces/:workspaceSlug/users/notifications/unread/ endpoint, ensuring the UI badge reflects the current server state. The store also updates this count optimistically when users mark individual notifications as read or unread.

What happens when a user marks a notification as read?

When markAsRead is invoked, the service sends a POST request to the read endpoint. Upon success, the store immediately calls mutateNotification on the local Notification instance to update the read_at timestamp. Because the store uses MobX observables, React components consuming this data re-render instantly, providing real-time visual feedback without waiting for a full data refresh.

Where is the notification backend logic implemented?

The backend logic for creating notification records resides in [apps/api/plane/bgtasks/notification_task.py](https://github.com/makeplane/plane/blob/preview/apps/api/plane/bgtasks/notification_task.py). This background task generates notifications when triggering events occur (such as issue comments or mentions), while the frontend service layer in packages/services/src/workspace/notification.service.ts handles all client-side API interactions.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →