@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 cachegetNotification(id, options?): on-demand fetch of a single notificationremoveNotification(id, options?): deletes a notificationtoggleStatus({ id, status }, options?): marks one notificationtoggleAllStatus(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.