Realtime in-app notifications for React — powered by CloudSignal MQTT.
Drop-in components for notification bell, toast popups, notification feed, and channel subscriptions with hooks for fine-grained control.
npm install @cloudsignal/notifications @cloudsignal/mqtt-clientimport {
NotificationProvider,
NotificationBell,
NotificationToast,
} from "@cloudsignal/notifications";
export default function App() {
return (
<NotificationProvider
userId="user-123"
connection={{
host: "wss://connect.cloudsignal.app:18885/",
username: "alice@org_k7xm4pqr2n5t",
password: "alice-password",
}}
channels={["announcements"]}
>
<NotificationBell />
<NotificationToast />
<YourApp />
</NotificationProvider>
);
}| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
userId |
string |
Yes | — | Unique user identifier for inbox subscriptions |
connection |
NotificationConnectionConfig |
Yes | — | MQTT connection config (see below) |
channels |
string[] |
No | [] |
Channel IDs to auto-subscribe on mount |
maxNotifications |
number |
No | 50 |
Max notifications kept in memory |
onNotification |
(n: Notification) => void |
No | — | Callback fired on each incoming notification |
debug |
boolean |
No | false |
Enable debug logging to console |
<NotificationProvider
userId="user-123"
connection={{
host: "wss://connect.cloudsignal.app:18885/",
username: "alice@org_k7xm4pqr2n5t",
password: "alice-password",
}}
><NotificationProvider
userId="user-123"
connection={{
host: "wss://connect.cloudsignal.app:18885/",
organizationId: "org-uuid",
secretKey: "sk_...",
}}
>import { useSession } from "@supabase/auth-helpers-react";
function App() {
const session = useSession();
return (
<NotificationProvider
userId={session.user.id}
connection={{
host: "wss://connect.cloudsignal.app:18885/",
organizationId: "org-uuid",
externalToken: session.access_token,
tokenServiceUrl: "https://auth.cloudsignal.app",
}}
>
<YourApp />
</NotificationProvider>
);
}All hooks must be called inside a <NotificationProvider>.
| Hook | Returns | Description |
|---|---|---|
useNotifications() |
{ notifications, markAsRead, markAllAsRead, dismiss, clearAll } |
Full notification feed and actions |
useUnreadCount() |
{ count, reset } |
Lightweight unread badge counter |
useChannels() |
{ channels, subscribe, unsubscribe } |
Runtime channel subscription management |
useNotificationEvent(type, cb) |
void |
Filtered listener for a specific notification type |
const { notifications, markAsRead, markAllAsRead, dismiss, clearAll } = useNotifications();
// notifications: InternalNotification[] (newest first)
// markAsRead(id: string): void
// markAllAsRead(): void
// dismiss(id: string): void — remove from list
// clearAll(): void — clear all notificationsconst { count, reset } = useUnreadCount();
// count: number — current unread count
// reset(): void — marks all as readconst { channels, subscribe, unsubscribe } = useChannels();
// channels: string[] — currently subscribed channel IDs
// subscribe(channelId: string): void
// unsubscribe(channelId: string): voiduseNotificationEvent("order.shipped", (notification) => {
// Fires only for notifications with type === "order.shipped"
console.log("Order shipped:", notification.data);
});Bell icon with unread badge and dropdown list.
| Prop | Type | Default | Description |
|---|---|---|---|
maxVisible |
number |
5 |
Max notifications in dropdown |
onNotificationClick |
(n: InternalNotification) => void |
— | Click handler for a notification |
renderEmpty |
() => ReactNode |
— | Custom empty state |
className |
string |
— | CSS class for outer container |
<NotificationBell maxVisible={10} onNotificationClick={(n) => router.push(n.action?.url)} />Auto-renders incoming notifications as temporary toasts.
| Prop | Type | Default | Description |
|---|---|---|---|
position |
"top-right" | "top-left" | "bottom-right" | "bottom-left" |
"top-right" |
Toast position |
duration |
number |
5000 |
Auto-dismiss duration in ms |
maxVisible |
number |
3 |
Max simultaneous toasts |
onClick |
(n: Notification) => void |
— | Click handler |
renderToast |
(n: Notification) => ReactNode |
— | Custom toast renderer |
<NotificationToast position="bottom-right" duration={8000} />Scrollable notification feed.
| Prop | Type | Default | Description |
|---|---|---|---|
onNotificationClick |
(n: InternalNotification) => void |
— | Click handler |
renderItem |
(n: InternalNotification) => ReactNode |
— | Custom item renderer |
renderEmpty |
() => ReactNode |
— | Custom empty state |
className |
string |
— | CSS class |
<NotificationList
onNotificationClick={(n) => router.push(n.action?.url)}
renderEmpty={() => <p>No notifications yet</p>}
/>Single notification card. Used internally by NotificationBell and NotificationList.
| Prop | Type | Required | Description |
|---|---|---|---|
notification |
InternalNotification |
Yes | The notification to render |
onClick |
() => void |
No | Click handler |
onDismiss |
() => void |
No | Dismiss handler |
All notifications flow over MQTT topics under $notifications/:
| Topic | Purpose |
|---|---|
$notifications/{userId}/inbox |
Personal notifications |
$notifications/{userId}/inbox/{category} |
Categorized personal notifications |
$notifications/channels/{channelId} |
Channel broadcasts |
The provider automatically subscribes to $notifications/{userId}/inbox/# (wildcard covers base + categories) and each channel topic specified in the channels prop.
interface Notification {
id: string; // Unique notification ID
type: string; // e.g. "order.shipped", "payment.received"
title: string; // Short title
body: string; // Body text
icon?: string; // Icon URL or emoji
image?: string; // Rich image URL
action?: { // Call-to-action
label: string;
url: string;
};
category?: string; // For filtering (e.g. "orders", "payments")
channel?: string; // Channel ID for broadcasts
sender?: { // Who triggered this
name: string;
avatar?: string;
};
ts: number; // Timestamp in milliseconds
data?: Record<string, unknown>; // Arbitrary metadata
}Extends Notification with client-side state:
interface InternalNotification extends Notification {
isRead: boolean; // Whether the user has read this
receivedAt: number; // When the client received it
}@cloudsignal/notifications (this package)
└── @cloudsignal/mqtt-client (peer dependency — MQTT transport)
└── VerneMQ broker (CloudSignal infrastructure)
- Receive-only — the SDK never publishes to MQTT. Notifications are sent server-side via the CloudSignal API or by publishing directly with a secret key.
- 1Hz state sync — notification state is stored in refs and synced to React state at 1Hz for batched re-renders.
- QoS 1 — all subscriptions use QoS 1 for reliable delivery.
MIT