# Agent brief — Inovus Notifications V2

For app developers: copy this entire document into your coding agent’s context (or save it as AGENT.md / a Cursor rule in the consuming app). It is self-contained. Do not invent APIs that are not listed here.


# 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.

Production base URL: https://notifications.totumcloud.com
Interactive API docs: https://notifications.totumcloud.com/docs
Package: @inovus-medical/notifications


# 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 in browser code.
  • Call stop() from subscribe() on logout (React hook cleans up on unmount).

# Auth model (critical)

Caller Token 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 publish({ userGuid, ... }) to any user
Optional admin User token in a Cognito group listed in PUBLISHER_GROUPS Same as backend publish

Worker env:

  • PUBLISHER_SCOPES=notifications/publishnormal / required for M2M. Matches token scope. 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. Publish userGuid must equal that sub or the recipient will never see the row.

  • source is server-derived from the token (client_id or sub). Never send source in the body.

  • Ordinary users get 403 on POST /v1/notifications (cross-user). Use /self or 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 only)

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
});

# dedupeKey rules (do not ignore)

  • Idempotency key scoped as (userGuid, source, dedupeKey) in the DB.
  • Always set a stable domain key for jobs (review_case:{id}, export:job:{id}, …).
  • Same key → 200 + { duplicate: true } + original row (treat as success).
  • New key → 201 + { duplicate: false }.
  • Without dedupeKey, the client will not auto-retry publish (prevents accidental duplicates). Convenience self-notify helpers add their own unique key per call.

# 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 in the notifications repo.

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 /v1/notifications publisher → any user
POST /v1/notifications/self any user → self
GET /v1/notifications own inbox (+ unreadCount, ETag)
GET /v1/notifications/unread-count badge
POST /v1/notifications/{id}/read own
POST /v1/notifications/{id}/unread own
POST /v1/notifications/mark-all-read own
DELETE /v1/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:

  1. Legacy HTTP /api/Notification/* — still served; often only base URL changes; Create needs a publisher Cognito token (no shared API keys). Envelope { value, errors, warnings }.
  2. Legacy façade notification.success(title, message, url?) — reimplement with createNotificationService(client) as shown above. Keep toast.* in the app.

Do not mix V1 envelopes with V2 client calls.


# Client options

Option Default Notes
baseUrl required No trailing slash needed
getAccessToken required string | null | Promise<…>
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/notifications rather than raw fetch unless required.
  • Set dedupeKey on every backend publish.
  • Match userGuid to Cognito sub.
  • Own the bell UI and toast UI in the app.
  • Unsubscribe polls on logout.
  • Prefer notification.* / publishSelf for browser self-notifies.

Don’t

  • Grant every end-user the notifications/publish scope so the SPA can notify arbitrary users.
  • Ship M2M secrets to the browser.
  • Expect WebSocket realtime.
  • Send source or 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 publisher token, or use /self
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 useNotifications used; 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. Human docs: user guide under docs/userguide/ in the notifications repo.