Skip to content

Notifications

@granit/notifications provides a transport-agnostic notification system — types, API functions, and a NotificationTransport interface for plugging in real-time channels. @granit/react-notifications wraps this into a provider with hooks for the notification inbox, unread count, activity feed, and user preferences.

Four transport packages implement the NotificationTransport interface: SignalR (WebSocket + LongPolling), SSE (Server-Sent Events), Web Push (VAPID), and mobile push (Capacitor).

Peer dependencies: axios, react ^19, @tanstack/react-query ^5

  • Directory@granit/notifications/ Types, API functions, transport interface (framework-agnostic)
    • @granit/react-notifications Provider, inbox/feed/preferences hooks
    • @granit/notifications-signalr SignalR transport (WebSocket + LongPolling)
    • @granit/notifications-sse SSE transport (Server-Sent Events)
    • Directory@granit/notifications-web-push/ Web Push VAPID subscription API
      • @granit/react-notifications-web-push useWebPush hook
    • Directory@granit/notifications-mobile-push/ Mobile push token registration API
      • @granit/react-notifications-mobile-push useMobilePush hook (Capacitor)
PackageRoleDepends on
@granit/notificationsDTOs, API functions, NotificationTransport interfaceaxios
@granit/react-notificationsNotificationProvider, inbox/feed/preferences hooks@granit/notifications, @granit/react-querying, react
@granit/notifications-signalrcreateSignalRTransport() factory@granit/notifications, @microsoft/signalr
@granit/notifications-ssecreateSseTransport() factory@granit/notifications, @microsoft/fetch-event-source
@granit/notifications-web-pushregisterPushSubscription(), VAPID helpersaxios
@granit/react-notifications-web-pushuseWebPush() hook@granit/notifications-web-push, react
@granit/notifications-mobile-pushregisterDeviceToken(), unregisterDeviceToken()axios
@granit/react-notifications-mobile-pushuseMobilePush() hook@granit/notifications-mobile-push, @capacitor/push-notifications, react
graph TD
    core["@granit/notifications"]
    react["@granit/react-notifications"]
    signalr["@granit/notifications-signalr"]
    sse["@granit/notifications-sse"]
    webPush["@granit/notifications-web-push"]
    reactWebPush["@granit/react-notifications-web-push"]
    mobilePush["@granit/notifications-mobile-push"]
    reactMobilePush["@granit/react-notifications-mobile-push"]

    react --> core
    signalr --> core
    sse --> core
    reactWebPush --> webPush
    reactMobilePush --> mobilePush

    style signalr fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
    style sse fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
    style webPush fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
    style mobilePush fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
import { NotificationProvider } from '@granit/react-notifications';
import { createSignalRTransport } from '@granit/notifications-signalr';
import { api } from './api-client';
const transport = createSignalRTransport({
hubUrl: '/hubs/notifications',
tokenGetter: () => keycloak.token,
});
function App() {
return (
<NotificationProvider config={{ apiClient: api }} transport={transport}>
<NotificationBell />
</NotificationProvider>
);
}
type NotificationSeverity = 'info' | 'success' | 'warning' | 'error';
interface NotificationDto {
id: string;
title: string;
body: string | null;
severity: NotificationSeverity;
entityType: string | null;
entityId: string | null;
isRead: boolean;
createdAt: string;
readAt: string | null;
}
interface NotificationPageDto {
items: NotificationDto[];
totalCount: number;
nextCursor: string | null;
unreadCount: number;
}
interface ActivityFeedEntryDto {
id: string;
title: string;
body: string | null;
severity: NotificationSeverity;
createdAt: string;
userId: string | null;
userDisplayName: string | null;
}
const NotificationChannels = {
InApp: 'inApp',
Email: 'email',
Sms: 'sms',
WhatsApp: 'whatsApp',
Push: 'push',
MobilePush: 'mobilePush',
Sse: 'sse',
SignalR: 'signalR',
Zulip: 'zulip',
} as const;
interface NotificationPreferenceDto {
notificationType: string;
label: string;
channels: Record<string, boolean>;
}
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
interface NotificationTransport {
connect(): Promise<void>;
disconnect(): Promise<void>;
readonly state: ConnectionState;
onNotification(callback: (notification: NotificationDto) => void): () => void;
onStateChange(callback: (state: ConnectionState) => void): () => void;
}

Any transport implementing this interface can be plugged into NotificationProvider.

FunctionEndpointDescription
fetchNotifications(client, basePath, params?)GET /notificationsPaginated inbox
markAsRead(client, basePath, id)PATCH /notifications/{id}/readMark single as read
markAllAsRead(client, basePath)POST /notifications/read-allMark all as read
fetchUnreadCount(client, basePath)GET /notifications/unread-countUnread count
fetchEntityActivityFeed(client, basePath, entityType, entityId, params?)GET /activity-feed/{type}/{id}Entity activity feed
fetchPreferences(client, basePath)GET /notification-preferencesUser preference matrix
updatePreference(client, basePath, preference)PUT /notification-preferences/{type}Update preference
interface SignalRTransportConfig {
readonly hubUrl: string;
readonly tokenGetter?: () => Promise<string | null>;
}
function createSignalRTransport(config: SignalRTransportConfig): NotificationTransport;

WebSocket (priority) with LongPolling fallback. Listens to the ReceiveNotification hub event. Auto-reconnects with token refresh on each reconnection.

interface SseTransportConfig {
readonly streamUrl: string;
readonly tokenGetter?: () => Promise<string | null>;
readonly heartbeatTypeName?: string; // default: '__heartbeat__'
}
function createSseTransport(config: SseTransportConfig): NotificationTransport;

Uses @microsoft/fetch-event-source for auto-reconnection. Heartbeat events are filtered automatically. Connection persists in background tabs (openWhenHidden: true).

interface NotificationProviderProps {
children: React.ReactNode;
config: NotificationConfig;
transport?: NotificationTransport;
}
function NotificationProvider(props: NotificationProviderProps): JSX.Element;

Returns connection state, last received notification, and unread count from the provider.

Paginated inbox with mark-as-read support and infinite scroll via @granit/react-querying.

interface UseNotificationsReturn {
notifications: readonly NotificationDto[];
totalCount: number;
loading: boolean;
loadingMore: boolean;
hasMore: boolean;
loadMore: () => void;
refresh: () => void;
markRead: (id: string) => Promise<void>;
markAllRead: () => Promise<void>;
}

Unread badge count with configurable polling interval (default: 60 seconds).

interface UseUnreadCountReturn {
count: number;
refresh: () => void;
}

Activity feed scoped to a specific entity (e.g. all actions on an invoice).

interface UseEntityActivityFeedReturn {
entries: readonly ActivityFeedEntryDto[];
totalCount: number;
loading: boolean;
hasMore: boolean;
loadMore: () => void;
refresh: () => void;
}

User notification preference matrix (type × channel) with toggle support.

interface UseNotificationPreferencesReturn {
preferences: NotificationPreferenceDto[];
loading: boolean;
saving: boolean;
toggleChannel: (notificationType: string, channel: string, enabled: boolean) => Promise<void>;
refresh: () => void;
}
// Register a Web Push subscription with the backend
async function registerPushSubscription(
client: AxiosInstance, basePath: string, subscription: PushSubscriptionJSON
): Promise<void>;
// Unregister a subscription
async function unregisterPushSubscription(
client: AxiosInstance, basePath: string, endpoint: string
): Promise<void>;
// Convert URL-safe Base64 VAPID key to Uint8Array for pushManager.subscribe()
function urlBase64ToUint8Array(base64String: string): Uint8Array;
interface WebPushConfig {
readonly vapidPublicKey: string;
readonly apiClient: AxiosInstance;
readonly basePath?: string;
readonly serviceWorkerPath?: string; // default: '/sw.js'
}
interface UseWebPushReturn {
readonly isSupported: boolean;
readonly permission: NotificationPermission;
readonly isSubscribed: boolean;
readonly loading: boolean;
subscribe: () => Promise<void>;
unsubscribe: () => Promise<void>;
}
function useWebPush(config: WebPushConfig): UseWebPushReturn;
type MobilePlatform = 'android' | 'ios';
interface DeviceTokenDto {
readonly token: string;
readonly platform: MobilePlatform;
readonly deviceId?: string;
}
async function registerDeviceToken(client, basePath, payload: DeviceTokenDto): Promise<void>;
async function unregisterDeviceToken(client, basePath, token: string): Promise<void>;
interface UseMobilePushReturn {
readonly isRegistered: boolean;
readonly loading: boolean;
register: () => Promise<void>;
unregister: () => Promise<void>;
}
function useMobilePush(config: MobilePushConfig): UseMobilePushReturn;

Requires @capacitor/push-notifications. Handles automatic token refresh — unregisters the old token and registers the new one on device-side token rotation.

CategoryKey exportsPackage
Core typesNotificationDto, NotificationSeverity, ConnectionState, NotificationChannels@granit/notifications
TransportNotificationTransport, NotificationConfig@granit/notifications
API functionsfetchNotifications(), markAsRead(), markAllAsRead(), fetchUnreadCount()@granit/notifications
Activity feedfetchEntityActivityFeed(), ActivityFeedEntryDto@granit/notifications
PreferencesfetchPreferences(), updatePreference(), NotificationPreferenceDto@granit/notifications
SignalRcreateSignalRTransport()@granit/notifications-signalr
SSEcreateSseTransport()@granit/notifications-sse
ProviderNotificationProvider, useNotificationContext()@granit/react-notifications
Inbox hooksuseNotifications(), useUnreadCount()@granit/react-notifications
Feed hooksuseEntityActivityFeed(), useNotificationPreferences()@granit/react-notifications
Web PushuseWebPush(), registerPushSubscription(), urlBase64ToUint8Array()@granit/react-notifications-web-push, @granit/notifications-web-push
Mobile PushuseMobilePush(), registerDeviceToken()@granit/react-notifications-mobile-push, @granit/notifications-mobile-push