Skip to content

@msflib/react-notification

Headless notification state for React apps, backed by the configured API.

Install

pnpm add @msflib/react-notification @msflib/core @msflib/react-shared

Peer dependencies

  • react >= 18
  • @tanstack/react-query >= 5

API Reference

NotificationProvider

A React context provider that manages the notification list and operations. Must be rendered inside a QueryClientProvider.

import { NotificationProvider } from '@msflib/react-notification';

<QueryClientProvider client={qc}>
  <NotificationProvider>{/* your app */}</NotificationProvider>
</QueryClientProvider>;

Props

Prop Type Required Description
children React.ReactNode Yes Child component tree.
options object No Configuration options for the provider. See below for details.

options Configuration

Key Type Default Description
isWorkspaceScoped boolean true If true, notifications are isolated per workspace.
requireAuth boolean false If true, notifications will only be fetched when isAuthenticated is true.
isAuthenticated boolean false The authentication status of the user (e.g. from useAuth().status).
enableAdminAccess boolean false If true, also loads the admin-sent notifications list on mount (see below).
query object — { offset?, limit? } used for the admin notifications list fetched on mount.

In Next.js App Router, NotificationProvider must be in a client component ("use client").

Hook API

const { notifications, removeNotification, toggleStatus, toggleAllStatus } =
  useNotification();
  • notifications: current notification list from query cache
  • getNotification(id, options?): on-demand fetch of a single notification
  • removeNotification(id, options?): deletes a notification
  • toggleStatus({ id, status }, options?): marks one notification
  • toggleAllStatus(status, options?): marks all notifications

For a declarative, cache-backed single-notification read instead of an imperative call, use the standalone useGetNotification(notificationId) hook:

import { useGetNotification } from '@msflib/react-notification';

function NotificationDetailView({ notificationId }: { notificationId: number }) {
  const { data: notification, isPending } = useGetNotification(notificationId);

  if (isPending) return <p>Loading…</p>;
  return <h3>{notification?.notification.title}</h3>;
}

useGetNotification is disabled (no request fired) while notificationId is undefined.

Admin notification access

Alongside the user-facing /notifications routes, the backend exposes /admin/notifications routes for admin surfaces — send a notification to specific accounts (or everyone on the platform), and inspect/remove admin-sent notifications. Unlike everything else in this module, these routes are global — not workspace-scoped (there's no {workspace_slug} in the path; isWorkspaceScoped on the provider has no effect on them). They're off by default; opt in with enableAdminAccess, the same way @msflib/react-tasks/@msflib/react-lottery gate their admin flows:

<NotificationProvider options={{ enableAdminAccess: true }}>
  {children}
</NotificationProvider>

When enabled, the provider automatically loads GET /admin/notifications/all (paginated via options.query) into adminNotifications. The send/get/delete/list-on-demand actions are always exposed on context regardless of this flag — only the automatic fetch is gated:

const {
  adminNotifications,
  sendNotification,
  listAdminNotifications,
  getAdminNotification,
  deleteAdminNotification,
} = useNotification();

async function broadcast() {
  await sendNotification({
    data: {
      title: 'Maintenance window',
      message: 'The platform will be down for maintenance at midnight.',
      channels: ['inapp', 'email'],
    },
    // Omit account_receiver_ids to send to every account on the platform.
    account_receiver_ids: [1, 2, 3],
  });
}

For a declarative, cache-backed single-notification read instead of an imperative call, use the standalone useAdminNotification(notificationId) hook:

import { useAdminNotification } from '@msflib/react-notification';

function AdminNotificationDetail({ notificationId }: { notificationId: number }) {
  const { data: notification, isPending } = useAdminNotification(notificationId);

  if (isPending) return <p>Loading…</p>;
  return <h3>{notification?.title}</h3>;
}

useAdminNotification is disabled (no request fired) while notificationId is undefined.

Multi-Tenancy behavior

This module follows the same workspace flow as other msflib React packages.

  • API paths are workspace-aware via configuredApiClient() from @msflib/core.
  • Query keys are workspace-scoped: [workspace ?? '__no_workspace__', 'msflib', 'notifications'].
  • Runtime workspace changes are observed through useActiveWorkspace() from @msflib/react-shared, so switching workspace re-fetches notifications automatically.

Example workspace switch:

import { setActiveWorkspace } from '@msflib/core';

setActiveWorkspace('acme');

Example

import { useNotification } from '@msflib/react-notification';

function NotificationActions() {
  const { notifications, toggleStatus, toggleAllStatus, removeNotification } =
    useNotification();

  return (
    <div>
      <button onClick={() => toggleAllStatus(true)}>Mark all as read</button>

      {notifications.map((item) => (
        <div key={item.notification_id}>
          <span>{item.notification.title}</span>
          <button
            onClick={() =>
              toggleStatus({ id: item.notification_id, status: true })
            }
          >
            Read
          </button>
          <button onClick={() => removeNotification(item.notification_id)}>
            Delete
          </button>
        </div>
      ))}
    </div>
  );
}

Types

export type NotificationType = 'system' | 'admin' | (string & {});

export type NotificationChannel =
  | 'inapp'
  | 'push'
  | 'email'
  | 'sms'
  | 'discord'
  | (string & {});

export type NotificationDetail = {
  id: number;
  created_at: string;
  updated_at: string;
  title: string;
  message: string;
  channels: NotificationChannel[];
  workspace_id?: number;
  sender_id: number;
  notification_type: NotificationType;
};

export type Notification = {
  receiver_id: number;
  notification_id: number;
  is_read: boolean;
  notification: NotificationDetail;
};

export type AdminNotificationCreatePayload = {
  title: string;
  message: string;
  channels: NotificationChannel[];
};

export type SendNotificationPayload = {
  data: AdminNotificationCreatePayload;
  /** Omit to send to every account on the platform. */
  account_receiver_ids?: number[];
};

export type AdminNotificationListParams = {
  offset?: number;
  limit?: number;
};

Payload shape

{
  "receiver_id": 0,
  "notification_id": 0,
  "is_read": true,
  "notification": {
    "id": 0,
    "created_at": "2026-03-02T10:49:34.176Z",
    "updated_at": "2026-03-02T10:49:34.176Z",
    "title": "string",
    "message": "string",
    "channels": ["inapp"],
    "sender_id": 0,
    "notification_type": "system"
  }
}

Endpoints

Configure paths through configureApplication({ endpoints: { notification } }).

Key Default path Methods / routes
notifications /notifications list (GET); detail (GET on /{id}); mark one (PATCH on /{id}/mark); mark all (PATCH on /mark-all); delete (DELETE on /{id})
adminNotifications /admin/notifications list (GET on /all, supports offset/limit); send (POST); detail/delete on /{id} (GET/DELETE)

notifications is workspace-scoped. adminNotifications is not — it's a global, platform-wide route regardless of the provider's isWorkspaceScoped option.