@inovus-medical/notifications
v0.3.0
Published
Typed client for the Inovus notification service — reliable by default (retries, timeouts, ETag polling), with optional React hooks
Readme
@inovus-medical/notifications
Typed client for the Inovus Notifications service — a persistent in-app inbox (not toasts).
- Retries with backoff (publish retries only when you set a
dedupeKey— see below) - Per-request timeouts
- One-shot token refresh on
401 - ETag polling (~30s) that returns
304when nothing changed - Optional React hooks (
@inovus-medical/notifications/react)
npm install @inovus-medical/notifications
# or: pnpm add @inovus-medical/notifications| | |
|---|---|
| Production API | https://notifications.totumcloud.com |
| OpenAPI / Swagger | https://notifications.totumcloud.com/docs |
| This README | Humans integrating the npm package |
| Agent brief | Copy-paste for coding agents: AGENT.md |
| Full docs | docs index |
Who uses which credential
| App role | Credential | Use |
|---|---|---|
| SPA / browser | User Cognito access token | Inbox + notify self (publishSelf / notification.*) |
| Backend / job / BFF | Publisher API key or M2M JWT | publish({ userGuid, … }) to any user |
| SPA → another user | Your BFF only | Never put API keys or M2M secrets in the browser |
apiKey and getAccessToken are mutually exclusive — do not set both.
Quick start — inbox (browser)
import { NotificationsClient } from '@inovus-medical/notifications';
const client = new NotificationsClient({
baseUrl: 'https://notifications.totumcloud.com',
// Your existing Cognito wiring — called per request, and again after a 401.
getAccessToken: () => auth.getValidAccessToken(),
});
const stop = client.subscribe(
({ items, unreadCount }) => renderBell(items, unreadCount),
{ intervalMs: 30_000 },
);
// ALWAYS call stop() on logout / unmount
const { items, nextCursor, unreadCount } = await client.list({ unreadOnly: true, limit: 50 });
await client.unreadCount();
await client.markRead(id);
await client.markUnread(id);
await client.markAllRead();
await client.delete(id);Self-notify helpers (V1-shaped)
import { NotificationsClient, createNotificationService } from '@inovus-medical/notifications';
const client = new NotificationsClient({
baseUrl: 'https://notifications.totumcloud.com',
getAccessToken: () => auth.getValidAccessToken(),
});
export const notification = createNotificationService(client);
await notification.success('Export ready', 'Your file is ready.', '/exports/1');
await notification.error('Failed', error.message);
await notification.warning('Warning', 'Check this');
await notification.info('Info', 'Heads up');Each helper calls POST /v2/notifications/self (recipient = token sub) and adds its own unique dedupeKey so a retried helper call won’t create two items.
Your app still owns: toast UI, bell markup, and navigating on actionUrl.
React
import { useNotifications } from '@inovus-medical/notifications/react';
function Bell() {
const { notifications, unreadCount, loading, error, markRead, markUnread, markAllRead, remove } =
useNotifications(client);
// subscription + cleanup on unmount handled for you
}Quick start — publish (backend)
API key (easiest)
const publisher = new NotificationsClient({
baseUrl: 'https://notifications.totumcloud.com',
apiKey: process.env.NOTIFICATIONS_API_KEY!, // never NEXT_PUBLIC_ / never browser
});
await publisher.publish({
userGuid: 'cognito-sub-of-recipient', // MUST match the reader's Cognito `sub`
title: 'Review complete',
body: 'Your assessment feedback is ready.',
severity: 'success', // 'info' | 'success' | 'warning' | 'error'
actionUrl: '/video/abc/feedback',
dedupeKey: 'review_case:8d2e', // ALWAYS set for jobs
metadata: { caseId: '8d2e' }, // optional, ≤ 8 KB
});M2M token
Use getAccessToken that returns a Cognito client-credentials token with scope notifications/publish.
dedupeKey — why you should always set one
Think of dedupeKey as a label for this specific notification, chosen by your app (e.g. review_case:8d2e or export:job:99).
What goes wrong without it
- Your server sends “review complete” and the network drops before you see the reply.
- You (or this client) send it again.
- The user gets two identical inbox items.
What happens with a stable key
- First send creates the notification.
- Same recipient + same app + same
dedupeKeyagain → the API does not create a second row. You get success withduplicate: trueand the original item. - Safe to retry after timeouts,
429s, or crashes.
Rules of thumb
- Backend / jobs: always set a key that means something in your domain (
review_case:{id}, not a random UUID each attempt). - Same real-world event → same key. Different events → different keys.
- Without a
dedupeKey, this npm client will not automatically retrypublish(on purpose — retries would risk duplicates). - Browser helpers (
notification.success(...)) invent a unique key per call for you.
Errors
import { NotificationsApiError } from '@inovus-medical/notifications';
try {
await client.markRead(id);
} catch (err) {
if (err instanceof NotificationsApiError) {
err.status; // e.g. 404, 429
err.problem; // RFC 9457 { title, status, detail? }
}
}429 includes Retry-After; the client honours it when retrying.
Server caps (after auth): 300 req / 60s per user (inbox/self), 600 / 60s per publisher (cross-user publish).
Options
| Option | Default | Notes |
|---|---|---|
| baseUrl | — | Service origin (no trailing slash required) |
| getAccessToken | — | Sync/async JWT getter. Required unless apiKey is set |
| apiKey | — | Publisher secret. Backend only. Do not combine with getAccessToken |
| timeoutMs | 10000 | Per attempt |
| maxRetries | 3 | Extra attempts on 429/5xx/network; publish only if dedupeKey set |
| retryBaseDelayMs | 250 | Exponential backoff + full jitter |
| fetch | global | Injectable for tests / custom runtimes |
Works in browsers, Node ≥ 20, and edge runtimes (fetch / AbortController). React is an optional peer — only for /react.
Package exports
| Import | Exports |
|---|---|
| @inovus-medical/notifications | NotificationsClient, createNotificationService, NotificationsApiError, wire types |
| @inovus-medical/notifications/react | useNotifications |
For coding agents
- Paste the full AGENT.md into the agent’s context (or save it as a rule in the consuming app).
- Do not invent endpoints, fields, or auth rules outside that brief.
- Prefer this package over raw
fetch. - SPA = user token + inbox/self only. Cross-user = server/BFF + API key or M2M + stable
dedupeKey.
Human walkthroughs: Getting started · Publishing · App integration · Rate limiting
BFF samples in the monorepo: examples/publish-proxy-worker/, examples/publish-proxy-next/.
Releasing this package (maintainers)
Full guide: releasing.md (what a changeset is, why .changeset/ often only has config.json).
Short version: pnpm changeset → merge to master → merge Version Packages → GitHub Actions publishes to npm. Do not npm publish from your laptop.
