SDKs
Pick the right NotificationStream SDK for your platform and learn what they have in common.
| Package | Runs in | Use it for |
|---|---|---|
@notificationstream/js | Browsers (any framework or none), workers, Node | The client store and headless UI core, for your own UI in any framework |
@notificationstream/react | React 18+ on the web, including Next.js | Provider, hooks, bell, drawer, inbox |
@notificationstream/react-native | React Native 0.72+ | Provider, hooks, native bell, drawer, inbox |
@notificationstream/node | Your server: Node 20+, Bun, Deno, Cloudflare Workers | Sending notifications, managing project users and segments, signing user tokens |
The React and React Native packages are built on @notificationstream/js: its client
(polling, realtime, caching, optimistic actions and, on the web, sharing across tabs), its
headless UI core (item view models, click flow, feed controller) and, for React, its
stylesheet. Options and behaviour are the same everywhere; only the rendering differs.
Build your own renderer
The headless UI core is the logic the bundled UIs are made of, so a custom UI (Vue, Svelte, a canvas, a terminal) gets the same semantics with little code:
import {
badgeText,
createFeedController,
createItemModel,
createNotificationClient,
feedPhase,
handleNotificationClick,
navigateTo,
} from "@notificationstream/js";
const client = createNotificationClient({ publishableKey: "pk_…", user: { id: userId }, token });
const feed = createFeedController(client);
client.subscribe((state) => {
renderBadge(badgeText(state.unseenCount)); // null hides the badge
if (feedPhase(state) !== "items") return renderPlaceholder(feedPhase(state));
renderRows(state.items.map((item) => createItemModel(item)));
});
onDrawerOpen(() => feed.open()); // returns close(): call it when the drawer closes
onRowClick((item) => handleNotificationClick(client, item, { navigate: (url) => navigateTo(url) }));What the client SDKs have in common
- Same inputs. Every client SDK needs your publishable key, the signed-in
user(itsid, plus optional profile fields) and a user token your backend signed, or a function that fetches one. Optionally a contentlocale, athemeand UIlabels. - Lazy start. Nothing is fetched until something subscribes to the client (a component
mounts, or you call
subscribe) or you call a method that needs the feed, such asrefresh()ormarkAsRead(). - A store. The client keeps an immutable state snapshot:
status,items,unreadCount,unseenCount,hasMore,loadingMore,filter,config,error,realtime. - Optimistic actions. Marking read, archiving and so on update the state immediately,
then sync with the server. On failure the change is rolled back and
state.erroris set. Action promises never reject. - Explicit updates. Realtime (
realtime()/useRealtime(), Growth plan and up) refetches as soon as the feed changes; polling (poll()/usePolling()) checks on a timer and pauses while realtime is connected. Both run only while visible and share one connection or poller across browser tabs, and the last page is cached for instant reloads. See Polling, realtime and performance. - Safe rendering. Bodies support bold, italic and links, rendered without HTML; only
http(s),mailtoand relative links are kept. - Themes and labels. Dashboard theme, then your code theme; every UI string can be overridden for localization.