SDKs

Pick the right NotificationStream SDK for your platform and learn what they have in common.

PackageRuns inUse it for
@notificationstream/jsBrowsers (any framework or none), workers, NodeThe client store and headless UI core, for your own UI in any framework
@notificationstream/reactReact 18+ on the web, including Next.jsProvider, hooks, bell, drawer, inbox
@notificationstream/react-nativeReact Native 0.72+Provider, hooks, native bell, drawer, inbox
@notificationstream/nodeYour server: Node 20+, Bun, Deno, Cloudflare WorkersSending 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 (its id, plus optional profile fields) and a user token your backend signed, or a function that fetches one. Optionally a content locale, a theme and UI labels.
  • 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 as refresh() or markAsRead().
  • 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.error is 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), mailto and relative links are kept.
  • Themes and labels. Dashboard theme, then your code theme; every UI string can be overridden for localization.

Before you start