# Agent brief — Inovus Notifications V2
How to use this file
- Copy this entire document into your coding agent’s context, or save it as
AGENT.md/ a Cursor rule in the consuming app.- Treat it as the contract. Do not invent endpoints, body fields, or auth rules that are not listed here.
- Humans: start with the user guide. Agents: stay on this page.
- npm package README (API reference for the client): packages/client/README.md
# Mission
Integrate Inovus Notifications V2 — a standalone persistent in-app notification inbox. Apps publish notifications to users; users read/manage their own inbox via a typed npm client.
Naming: V2 / v2 = this service’s current HTTP API (/v2/notifications…). V1 / v1 = legacy product surface only (/api/Notification/*). Do not call the current API “v1”.
| Production base URL | https://notifications.totumcloud.com |
| Interactive API docs | https://notifications.totumcloud.com/docs |
| Current API prefix | /v2 |
| npm package | @inovus-medical/notifications |
| CORS | Any origin (auth still required) |
| App wiring / BFF | app-integration.md |
| Rate limits | After auth: 300 req/60s per user (inbox/self), 600/60s per publisher. Over → 429 + Retry-After: 60. Detail: rate-limiting.md |
# What this is / is not
| This service | Not this service |
|---|---|
| Persistent inbox (survives refresh) | Toast / snackbar UI |
| Badge unread count + mark read/delete | Bell dropdown styling / branding |
Optional actionUrl deep link field |
Router navigation (app owns that) |
| ~30s ETag polling by default | WebSocket / SSE “instant” realtime |
| Cognito JWT auth | New login system |
Toasts stay in the app. Use a toast for ephemeral “Saved!” feedback. Use this service when the user should still see the message later in a bell/inbox.
# Install
npm install @inovus-medical/notifications
# or: pnpm add @inovus-medical/notifications
React is an optional peer — only required for @inovus-medical/notifications/react.
# One-time wiring (do this once per app)
// lib/notifications.ts
import {
NotificationsClient,
createNotificationService,
} from '@inovus-medical/notifications';
export const notificationsClient = new NotificationsClient({
baseUrl: 'https://notifications.totumcloud.com', // or staging URL
// MUST return the current Cognito access token (sync or async).
// Called per request; called again once after 401 for refresh.
getAccessToken: () => auth.getValidAccessToken(),
});
/** V1-shaped helpers for self-notifications (current user only). */
export const notification = createNotificationService(notificationsClient);
Invariants for the agent:
- Construct one client; inject the app’s existing token getter — do not invent token storage.
- Never put Cognito M2M client secrets or publisher API keys in browser code.
- Call
stop()fromsubscribe()on logout (React hook cleans up on unmount).
# Auth model (critical)
| Caller | Credentials | Can do |
|---|---|---|
| End user (SPA) | User access token | List/manage own inbox; publishSelf / notification.success(...) to self only |
| Backend / job | M2M token with scope notifications/publish or X-Api-Key from PUBLISHER_API_KEYS |
publish({ userGuid, ... }) to any user |
| Optional admin | User token in a Cognito group listed in PUBLISHER_GROUPS |
Same as backend publish |
User JWT publish rules (same as Publishing):
| Credential | Self (/self) |
Cross-user (POST /v2/notifications) |
|---|---|---|
| User access token (normal login) | Yes | No (403), unless user is in PUBLISHER_GROUPS |
M2M token with notifications/publish |
No (use user token for inbox/self) | Yes |
Publisher X-Api-Key |
No (publish-only; inbox/self → 403) | Yes |
Worker env:
-
PUBLISHER_API_KEYS=name:secret,…— easiest publish path. HeaderX-Api-Key: <secret>. Publish-only. Seedocs/api-keys.md. -
PUBLISHER_SCOPES=notifications/publish— for Cognito M2M. Matches tokenscope. You create a Cognito resource server scope, not a user group. -
PUBLISHER_GROUPS=…— optional. Only if humans publish with user logins; then you create that Cognito group. Omit if unused. -
Inbox ownership key = Cognito JWT
sub. PublishuserGuidmust equal thatsubor the recipient will never see the row. -
sourceis server-derived from credentials (client_id/ API key name orsub). Never sendsourcein the body. -
Ordinary users get 403 on
POST /v2/notifications(cross-user). Use/selfor the convenience API instead.
# Consume — inbox
# List / mutate
import { notificationsClient } from './lib/notifications';
const { items, nextCursor, unreadCount } = await notificationsClient.list({
limit: 50,
unreadOnly: true,
});
await notificationsClient.markRead(id);
await notificationsClient.markUnread(id);
await notificationsClient.markAllRead();
await notificationsClient.delete(id);
const count = await notificationsClient.unreadCount();
list returns total unreadCount (not page-limited). Pagination via opaque nextCursor. Max limit 200.
# Subscribe (polling)
const stop = notificationsClient.subscribe(
({ items, unreadCount }) => {
/* update bell UI */
},
{ intervalMs: 30_000 },
);
stop(); // ALWAYS on logout
Behaviour: ETag/304 when unchanged; pauses when tab hidden; refreshes on visibility. This is polling, not WebSockets.
# React
import { useNotifications } from '@inovus-medical/notifications/react';
import { notificationsClient } from './lib/notifications';
const {
notifications,
unreadCount,
loading,
error,
markRead,
markUnread,
markAllRead,
remove,
} = useNotifications(notificationsClient);
App owns markup. On row click: markRead(id) then navigate if actionUrl is set.
# Self-notify (browser-safe)
When the current user should get an inbox item:
import { notification } from './lib/notifications';
await notification.success('Export ready', 'Your CSV is ready.', '/exports/1');
await notification.error('Failed', error.message);
await notification.warning('Warning', 'Check this');
await notification.info('Info', 'FYI');
// or: notification.notify('success', title, body, url?)
Equivalent low-level API:
await notificationsClient.publishSelf({
title: 'Export ready',
body: 'Your CSV is ready.',
severity: 'success',
actionUrl: '/exports/1',
// dedupeKey optional; convenience API supplies a unique per-call key
});
Recipient is always token sub. Cannot target another user.
# Publish to another user (backend / M2M or API key)
await notificationsClient.publish({
userGuid: recipientCognitoSub, // MUST be Cognito sub
title: 'Review complete',
body: 'Your assessment feedback is ready.',
severity: 'success', // 'info' | 'success' | 'warning' | 'error'
actionUrl: `/video/${videoId}/feedback`,
dedupeKey: `review_case:${caseId}`, // ALWAYS set for jobs
metadata: { caseId, videoId }, // optional, ≤ 8 KB JSON
});
# Easy path: API key (no Cognito M2M)
Worker: PUBLISHER_API_KEYS=assess:<long-random-secret> (see docs/api-keys.md).
const client = new NotificationsClient({
baseUrl: 'https://notifications.totumcloud.com',
apiKey: process.env.NOTIFICATIONS_API_KEY!,
});
# dedupeKey (always set for backend / jobs)
A label for this notification so retries don’t create two inbox items.
Why: if the request times out and you send again, without a key the user may get duplicates. With the same key (e.g. review_case:8d2e), the second call returns the original row and duplicate: true.
How to choose it: stable string from your domain — same real-world event → same key every time. Examples: review_case:${id}, export:job:${id}. Do not use a fresh random UUID per attempt.
Client behaviour: without dedupeKey, @inovus-medical/notifications will not auto-retry publish. With a key, retries on 429/5xx/network are safe. Convenience helpers (notification.success) add a unique key per call for you.
Matching is: recipient + your app identity (from credentials) + dedupeKey.
# M2M token (server)
$ClientId / $ClientSecret come from Cognito → user pool → App integration → App clients → (confidential client with a generated secret). Token host is App integration → Domain (not the issuer URL). Full console steps: docs/cognito-setup.md. Production M2M reference (safety / rollback): docs/authentication/cognito-m2m-notifications.md.
PowerShell (Windows) — use curl.exe:
$ClientId = "…"
$ClientSecret = "…"
$CognitoDomain = "https://<prefix>.auth.<region>.amazoncognito.com"
curl.exe -X POST "$CognitoDomain/oauth2/token" `
-H "Content-Type: application/x-www-form-urlencoded" `
-u "${ClientId}:${ClientSecret}" `
-d "grant_type=client_credentials&scope=notifications/publish"
Cache until expires_in. Wire via getAccessToken on a server-side client instance.
# Wire types (camelCase)
type Severity = 'info' | 'success' | 'warning' | 'error';
interface Notification {
id: string; // UUID
userGuid: string;
source: string;
title: string;
body: string;
severity: Severity;
actionUrl: string | null;
dedupeKey: string | null;
metadata: Record<string, unknown>;
read: boolean;
readAt: string | null;
createdAt: string; // ISO
}
Limits (approximate): title ≤ 200, body ≤ 2000, dedupeKey ≤ 256, metadata ≤ 8 KB. Unknown fields on V2 bodies → 400.
# HTTP cheat sheet (V2)
| Method | Path | Who |
|---|---|---|
POST |
/v2/notifications |
publisher → any user |
POST |
/v2/notifications/self |
any user → self |
GET |
/v2/notifications |
own inbox (+ unreadCount, ETag) |
GET |
/v2/notifications/unread-count |
badge |
POST |
/v2/notifications/{id}/read |
own |
POST |
/v2/notifications/{id}/unread |
own |
POST |
/v2/notifications/mark-all-read |
own |
DELETE |
/v2/notifications/{id} |
own |
GET |
/health |
public |
Errors: RFC 9457 application/problem+json. Client throws NotificationsApiError with status + problem.
# V1 compatibility (only if migrating)
Two different things:
- Legacy HTTP
/api/Notification/*— still served; often only base URL changes; Create needs a publisher (Cognito M2M/scope/group or namedX-Api-KeyfromPUBLISHER_API_KEYS— not the old VL anonymous shared key). Envelope{ value, errors, warnings }. - Legacy façade
notification.success(title, message, url?)— reimplement withcreateNotificationService(client)as shown above. Keeptoast.*in the app.
Do not mix V1 envelopes with V2 client calls.
# Client options
| Option | Default | Notes |
|---|---|---|
baseUrl |
required | No trailing slash needed |
getAccessToken |
— | User/M2M JWT getter. Required unless apiKey is set |
apiKey |
— | Publisher secret from PUBLISHER_API_KEYS. Backend only — never in browser. Mutually exclusive with getAccessToken |
timeoutMs |
10000 |
Per attempt |
maxRetries |
3 |
429/5xx/network; publish only if dedupeKey set |
retryBaseDelayMs |
250 |
Full jitter backoff |
fetch |
globalThis.fetch |
Injectable for tests |
Package exports: NotificationsClient, createNotificationService, NotificationsApiError, types, and useNotifications from /react.
# Do / don’t checklist for the agent
Do
- Use
@inovus-medical/notificationsrather than rawfetchunless required. - Set
dedupeKeyon every backend publish. - Match
userGuidto Cognitosub. - Own the bell UI and toast UI in the app.
- Unsubscribe polls on logout.
- Prefer
notification.*/publishSelffor browser self-notifies.
Don’t
- Grant every end-user the
notifications/publishscope so the SPA can notify arbitrary users. - Ship M2M secrets to the browser.
- Expect WebSocket realtime.
- Send
sourceor unknown fields on V2 publish bodies. - Build toast UI inside this integration “because the old quick-start had toastService”.
- Retry unkeyed publishes manually in a loop without a stable key.
# Common failures → fix
| Symptom | Fix |
|---|---|
401 |
Token missing/expired; fix getAccessToken / refresh |
403 on publish |
Need M2M / API key via backend, or use /self |
429 Too Many Requests |
Cap hit (300/60s user, 600/60s publish). Honour Retry-After. Client retries by default when allowed |
| Empty inbox after publish | userGuid ≠ reader’s sub, or wrong environment base URL |
| Duplicates | Missing/unstable dedupeKey |
| “Not live” | Polling ~30s by default — expected |
# Minimal acceptance criteria
- [ ] Client constructed with production/staging
baseUrl+ real token getter - [ ] Bell (or equivalent) lists items + unread count; mark read / delete work
- [ ] Subscribe or
useNotificationsused; cleaned up on logout/unmount - [ ] Backend jobs publish with M2M token + stable
dedupeKey - [ ] Browser self-notifies use convenience API or
publishSelf(not cross-user publish) - [ ] Toasts remain a separate app concern
End of agent brief. Prefer this document over inventing APIs.
Further reading (humans): docs index · Getting started · Publishing · API keys