# 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 /v1/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 token instead of old API key?
- Still expecting soft-delete restore? (not supported — see V1 compatibility)
Inspect errors[] in the envelope and the HTTP status.
# Local wrangler dev weirdness
- Memory store resets every restart
- Placeholder
AUTH_ISSUER→ all authed calls 401 until you set a real issuer - 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.