# Advanced usage

You’ve got the basics working. This page covers the knobs that matter once you’re in production.


# Polling, ETags, and “realtime”

Updates are not WebSocket push. The client polls, and the server makes idle polls cheap.

sequenceDiagram
  participant App
  participant API

  App->>API: GET /v1/notifications (If-None-Match: W/"abc")
  alt unchanged
    API-->>App: 304 Not Modified
  else changed items or unreadCount
    API-->>App: 200 + new ETag + items + unreadCount
    App->>App: update bell UI
  end
Behaviour Detail
Default interval 30 000 ms (SubscribeOptions.intervalMs)
Hidden tabs Polling pauses
Tab visible again Immediate refresh
Count-only changes Included in list ETag — badge stays honest even if the change is off the first page

Need sub-second delivery someday? The design allows SSE behind the same subscribe() API later — no need to rewrite call sites.


# Pagination and filters

const page1 = await client.list({ limit: 50 });
if (page1.nextCursor) {
  const page2 = await client.list({
    limit: 50,
    cursor: page1.nextCursor,
  });
}

await client.list({ unreadOnly: true });
await client.list({ since: '2026-07-01T00:00:00Z' }); // ISO with offset

Notes:

  • Keyset cursor (opaque string) — don’t parse it
  • Default page size 50, max 200 (server-enforced)
  • unreadCount on the list response is the total for the user, not “unread on this page”

# Retries and timeouts

Setting Default Meaning
timeoutMs 10000 Per attempt
maxRetries 3 Extra tries after the first
retryBaseDelayMs 250 Exponential backoff + full jitter
Publish retries only if dedupeKey set Prevents duplicate rows on lost responses

Reads and mark-read/delete are always safe to retry.
publish / publishSelf without a key fail fast on network/5xx instead of guessing.

401 handling: the client calls getAccessToken again once and retries (for refresh-token flows).


# Metadata

Use metadata for producer context that isn’t title/body:

await client.publish({
  userGuid,
  title: 'Review complete',
  body: 'Feedback is ready.',
  dedupeKey: `review_case:${caseId}`,
  metadata: {
    caseId,
    activityInstanceId,
    product: 'assess',
  },
});

Rules:

  • Opaque JSON object
  • Max ~8 KB serialized
  • Not queried by the service — your UI/app interprets it
  • V1 leftover fields land under metadata.v1 when using legacy Create

# Self-publish vs publisher publish

Self Publisher
Endpoint /v1/notifications/self /v1/notifications
Token Any user Publisher scope/group
Recipient Always sub Any userGuid
Typical caller Browser Backend job
Spoof risk None (forced to self) Trusted server only

# Multiple Cognito pools

AUTH_ISSUER accepts a comma-separated list. The Worker picks JWKS from the token’s iss claim. One pool per deployment is still the common case.


# Retention

A daily cron deletes notifications older than RETENTION_DAYS (default 90). Set 0 to disable.

Plan product copy accordingly (“older than 90 days may disappear”) if users expect permanent history.


# Custom client options

new NotificationsClient({
  baseUrl,
  getAccessToken,
  timeoutMs: 8_000,
  maxRetries: 2,
  retryBaseDelayMs: 200,
  fetch: myFetch, // tests / non-standard runtimes
});

# When to read the OpenAPI

Interactive contract: https://notifications.totumcloud.com/docs
Committed snapshot: packages/api/openapi.json

If the snapshot and the running service disagree, something’s wrong with the deploy — trust CI’s OpenAPI drift check.

Next: Troubleshooting or the Developer reference.