# Troubleshooting
Friendly fixes for the problems people actually hit.
# Quick decision tree
flowchart TD
Start["Something's wrong"] --> Auth{"401 or 403?"}
Auth -->|401| T401["Token missing / expired / wrong issuer"]
Auth -->|403| T403["Publishing without publisher rights"]
Auth -->|no| Empty{"Empty inbox?"}
Empty -->|yes| Guid["userGuid ≠ reader's Cognito sub"]
Empty -->|no| Dup{"Duplicates?"}
Dup -->|yes| Key["Missing or unstable dedupeKey"]
Dup -->|no| Live{"UI feels stale?"}
Live -->|yes| Poll["Polling is ~30s — not WebSockets"]
Live -->|no| Other["Check network tab + problem+json detail"]# Authentication & authorization
# 401 Unauthorized
getAccessTokenreturnednull/undefined- Token expired and refresh didn’t run
AUTH_ISSUERon the Worker doesn’t match the token’siss- Local
.dev.varsstill has the placeholder issuer
Try: decode the JWT (jwt.io) and compare iss / exp with Worker config.
# 403 Forbidden on publish
- Using a normal user token against
POST /v2/notifications - M2M client missing
notifications/publishscope PUBLISHER_SCOPES/PUBLISHER_GROUPSmisconfigured on the Worker
Try: self-notify via notification.success / publishSelf, or fix the M2M client.
# 404 on mark read / delete
- Notification id doesn’t exist
- It belongs to another user (the API returns 404 rather than 403 to avoid leaking existence)
# Empty inbox / “I published but they don’t see it”
- Confirm publish returned
201(or200duplicate) - Compare
notification.userGuidwith the reader’s tokensub— must match exactly - Confirm the reader is hitting the same environment (staging vs prod base URL)
- If using V1 CreatePayload templates, confirm
V1_PAYLOAD_TEMPLATESis set
# Duplicates
| Cause | Fix |
|---|---|
Job retries without dedupeKey |
Always set a stable domain key |
| Key changes on every attempt | Use something derived from the business id |
| Two different M2M clients, same logical event | Dedupe is per source — pick one publisher identity |
| Convenience API called twice intentionally | Expected — each call gets a unique key |
Remember: without dedupeKey, the npm client won’t retry publish (on purpose).
# “Realtime isn’t realtime”
That’s expected. Default freshness is ~30 seconds, plus:
- Hidden tabs don’t poll
- Visible tabs refresh immediately on focus
Turn the interval down if product needs it (intervalMs: 10_000), understanding cost vs freshness. True push isn’t in V2 yet.
# Badge count looks wrong
List responses include total unreadCount in the ETag payload. If you’re still on a very old client that called unreadCount() only after list changes, upgrade the client.
Also check optimistic UI in the React hook — the next poll reconciles.
# V1 envelope callers broken after cutover
- Base URL updated?
- Publisher credentials (Cognito M2M/scope or named
X-Api-KeyfromPUBLISHER_API_KEYS) instead of the old VL anonymous shared key? - Still expecting soft-delete restore? (not supported — see V1 compatibility)
Inspect errors[] in the envelope and the HTTP status.
# Rate limits / 429
See Rate limiting for what we enforce and the 429 response.
Inbox/self: 300 requests / 60s per user. Cross-user publish: 600 / 60s per publisher. Over the limit → problem+json with Retry-After: 60. The npm client retries 429 automatically when retries are allowed.
# Browser “Failed to fetch” / CORS
The API allows any origin in the Worker (cors middleware). After deploying a build that includes that middleware:
- Confirm you’re calling the API host (e.g.
https://notifications.totumcloud.com), not the static docs Worker - Retry Swagger Try it out or your SPA
- If it still fails, check with
curl.exe— if curl works and the browser doesn’t, it’s still a browser/CORS/extension issue; if curl fails too, fix auth/status first
# Local wrangler dev weirdness
- Memory store resets every restart
- Missing / wrong Worker Variables (
AUTH_ISSUER,SUPABASE_URL) → fix in the dashboard (not inwrangler.toml) - Open http://localhost:8787/health first
# How to read an error
V2 routes return problem+json:
{
"type": "about:blank",
"title": "Forbidden",
"status": 403,
"detail": "Publishing requires a publisher group or scope"
}
In the client: err instanceof NotificationsApiError → err.status, err.problem.
# Still stuck?
- Hit
/health— is storage up? - Compare behaviour in
/docsSwagger with the same token - Check Worker logs in the Cloudflare dashboard
- Skim invariants in CLAUDE.md / DESIGN.md
For extending the system itself, continue to Developer reference.