# How the Plane Notification System Works Across Workspaces and Projects

> Discover how Plane's notification system uses Django, REST APIs, and a TypeScript frontend to deliver real-time workspace-specific alerts with project-level filtering. Understand its architecture.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: internals
- Published: 2026-06-22

---

**Plane's notification system leverages Django models with workspace-scoped foreign keys, REST API endpoints, and a TypeScript frontend service to deliver real-time, isolated notifications per workspace while supporting granular project-level filtering.**

Plane is an open-source project management platform developed by makeplane/plane that handles complex multi-tenant environments. Its notification system is architected to strictly isolate data between workspaces while allowing optional project-level granularity, ensuring users never receive cross-workspace alerts. The implementation spans backend models, REST APIs, and frontend state management to maintain this isolation.

## Backend Data Models and Database Schema

The foundation of the system resides in [`apps/api/plane/db/models/notification.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/notification.py), which defines three core models that handle notification storage, user preferences, and email audit trails.

### The Notification Model

Every notification is tied to a workspace via a mandatory foreign key and optionally to a project. The model includes fields for `workspace`, `project`, `entity_identifier`, `entity_name`, `title`, `message`, `sender`, `receiver`, `read_at`, `snoozed_till`, and `archived_at`. The database index `notif_receiver_state_idx` ensures efficient filtering by receiver and status fields.

### User Preferences and Email Logging

The `UserNotificationPreference` model stores opt-in settings per workspace and project, including boolean flags for `property_change`, `state_change`, `comment`, `mention`, and `issue_completed`. The `EmailNotificationLog` model persists sent emails for audit purposes with fields like `receiver`, `triggered_by`, `entity_identifier`, `data`, `sent_at`, and `processed_at`.

## API Architecture and Workspace Scoping

The backend exposes REST endpoints that strictly scope all queries by workspace slug. Key endpoints include:

- `GET /api/workspaces/{workspaceSlug}/users/notifications/unread/` – Returns unread notification counts
- `GET /api/workspaces/{workspaceSlug}/users/notifications` – Returns paginated notification lists
- `PATCH /api/workspaces/{workspaceSlug}/users/notifications/{notificationId}/` – Updates notification status
- `POST /api/workspaces/{workspaceSlug}/users/notifications/{notificationId}/read/` – Marks as read
- `DELETE /api/workspaces/{workspaceSlug}/users/notifications/{notificationId}/read/` – Marks as unread
- `POST /api/workspaces/{workspaceSlug}/users/notifications/{notificationId}/archive/` – Archives a notification
- `POST /api/workspaces/{workspaceSlug}/users/notifications/mark-all-read/` – Bulk marks notifications as read

## Frontend TypeScript Service Layer

The `WorkspaceNotificationService` class in [`packages/services/src/workspace/notification.service.ts`](https://github.com/makeplane/plane/blob/main/packages/services/src/workspace/notification.service.ts) extends `APIService` to provide typed methods that enforce workspace isolation. Every method requires a `workspaceSlug` parameter.

```typescript
// packages/services/src/workspace/notification.service.ts
export class WorkspaceNotificationService extends APIService {
  async getUnreadCount(workspaceSlug: string) { 
    // Returns TUnreadNotificationsCount
  }
  
  async list(workspaceSlug: string, params: any) { 
    // Supports filtering by project, is_read, etc.
  }
  
  async markAsRead(workspaceSlug: string, notificationId: string) { }
  async markAsUnread(workspaceSlug: string, notificationId: string) { }
  async archive(workspaceSlug: string, notificationId: string) { }
  async unarchive(workspaceSlug: string, notificationId: string) { }
  async markAllAsRead(workspaceSlug: string, filter: any) { }
}

```

## State Management with MobX

The frontend caches notification data in [`apps/web/core/store/notifications/notification.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/store/notifications/notification.ts). This MobX store maintains paginated lists, unread counts, and UI flags per workspace. When users switch workspaces, the store fetches fresh data, preventing cross-workspace notification leakage.

```typescript
// Fetching notifications for current workspace
await notificationStore.fetchNotifications({
  workspaceSlug: currentWorkspace.slug,
  page: 1,
  per_page: 20,
  project: "proj-123" // optional filter
});

// Marking as read updates local cache optimistically
await notificationStore.markAsRead(notification.id);

```

## Cross-Workspace and Cross-Project Isolation

Plane implements strict isolation through foreign key relationships and query filtering. Each `Notification` row stores a `workspace_id`, and all API queries filter on this field. Project-level filtering is optional via the nullable `project` field.

The background task in [`apps/api/plane/bgtasks/notification_task.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/bgtasks/notification_task.py) checks `UserNotificationPreference` before dispatching emails, respecting user settings per workspace and project. When an event occurs, the system creates a notification row pointing to the relevant workspace and optional project, then filters queries accordingly.

## Implementation Examples

### Fetching Unread Counts

```typescript
import { WorkspaceNotificationService } from "@plane/services";

const notifService = new WorkspaceNotificationService();

async function loadUnreadCount(workspaceSlug: string) {
  const data = await notifService.getUnreadCount(workspaceSlug);
  console.log(`Unread notifications in ${workspaceSlug}:`, data?.count ?? 0);
}

```

### Filtering Notifications by Project

```typescript
await notifService.list("my-workspace", {
  page: 1,
  per_page: 20,
  project: "proj-123",   // optional project filter
  is_read: false,        // only unread items
});

```

### Backend Notification Creation

```python

# apps/api/plane/bgtasks/notification_task.py

def create_notification(workspace, receiver, **kwargs):
    Notification.objects.create(
        workspace=workspace,
        project=kwargs.get("project"),
        entity_identifier=kwargs.get("entity_id"),
        entity_name=kwargs.get("entity_name"),
        title=kwargs["title"],
        message=kwargs["message"],
        sender=kwargs["sender"],
        receiver=receiver,
    )

```

### Bulk Marking as Read

```typescript
await notifService.markAllAsRead("my-workspace", {
  project: "proj-123",
  is_read: false,
});

```

## Summary

- **Workspace isolation** is enforced through mandatory `workspace` foreign keys in the `Notification` model and workspace-scoped API endpoints
- **Project granularity** is achieved through an optional nullable `project` field that allows secondary filtering within a workspace
- **Performance optimization** comes from database indexes like `notif_receiver_state_idx` that speed up read/unread queries
- **User preferences** are stored per workspace and project in `UserNotificationPreference`, checked by background tasks before email dispatch
- **Frontend architecture** uses `WorkspaceNotificationService` and MobX stores in [`apps/web/core/store/notifications/notification.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/store/notifications/notification.ts) to maintain per-workspace caches

## Frequently Asked Questions

### How does Plane prevent notifications from leaking between workspaces?

Plane enforces isolation through database-level foreign keys and API-level filtering. Every notification row includes a `workspace` field, and all REST endpoints require a `workspaceSlug` parameter. The frontend `WorkspaceNotificationService` and MobX store are instantiated per workspace, ensuring complete data separation between different workspaces.

### Can users customize notification preferences for specific projects?

Yes. The `UserNotificationPreference` model supports granular settings for each workspace and project combination. Users can toggle specific triggers like `property_change`, `comment`, `mention`, or `issue_completed` independently. The background task in [`notification_task.py`](https://github.com/makeplane/plane/blob/main/notification_task.py) checks these preferences before dispatching emails, ensuring users only receive notifications they have opted into for that specific project.

### What happens when a user marks all notifications as read?

The `markAllAsRead()` method sends a POST request to `/api/workspaces/{workspaceSlug}/users/notifications/mark-all-read/` with optional filter parameters. The backend updates the `read_at` timestamp for all matching notifications, while the frontend optimistically clears the unread count in the MobX store and refreshes the notification list.

### Where are notification email logs stored for audit purposes?

Email dispatches are recorded in the `EmailNotificationLog` model defined in [`apps/api/plane/db/models/notification.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/notification.py). This table stores the `receiver`, `triggered_by` event, `entity_identifier`, payload `data`, and timestamps for `sent_at` and `processed_at`. This enables audit trails and retry mechanisms for failed deliveries.