How the Plane Notification System Works Across Workspaces and Projects
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, 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 countsGET /api/workspaces/{workspaceSlug}/users/notifications– Returns paginated notification listsPATCH /api/workspaces/{workspaceSlug}/users/notifications/{notificationId}/– Updates notification statusPOST /api/workspaces/{workspaceSlug}/users/notifications/{notificationId}/read/– Marks as readDELETE /api/workspaces/{workspaceSlug}/users/notifications/{notificationId}/read/– Marks as unreadPOST /api/workspaces/{workspaceSlug}/users/notifications/{notificationId}/archive/– Archives a notificationPOST /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 extends APIService to provide typed methods that enforce workspace isolation. Every method requires a workspaceSlug parameter.
// 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. 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.
// 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 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
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
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
# 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
await notifService.markAllAsRead("my-workspace", {
project: "proj-123",
is_read: false,
});
Summary
- Workspace isolation is enforced through mandatory
workspaceforeign keys in theNotificationmodel and workspace-scoped API endpoints - Project granularity is achieved through an optional nullable
projectfield that allows secondary filtering within a workspace - Performance optimization comes from database indexes like
notif_receiver_state_idxthat 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
WorkspaceNotificationServiceand MobX stores inapps/web/core/store/notifications/notification.tsto 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 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. 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.
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 →