# notifications — the in-app feed and unread badge

> Client seam `client/src/services/notifications/` · `USE_NOTIFICATIONS_MOCK = false` (**real**) · 4 server ops
> Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29).

Server-raised, user-scoped notifications. In-app only — there is no push channel and no email channel.

## Endpoints

| Method | Path | Verdict |
| --- | --- | --- |
| GET | `/api/v1/notifications/get_notifications` | wired · paginated |
| GET | `/api/v1/notifications/get_unread_count` | wired — polled for the bell badge |
| POST | `/api/v1/notifications/mark_notification_read` | wired |
| POST | `/api/v1/notifications/mark_all_read` | wired |

All `[Authorize]`. No phantoms. The domain maps 1:1.

## Shape rules the JSON does not express

- **`dataJson` is a string containing JSON**, not an object. It is the deep-link payload: the client parses
  it (`parse.ts`) and resolves a route from it (`deepLink.ts`), both with unit tests, and **falls back to
  an inert notification rather than throwing** on anything unrecognised. A notification whose payload
  cannot be parsed still renders — it just is not tappable.
- **Unread count is a separate read, not derived from the list.** The badge must be correct without
  fetching a page, so `get_unread_count` is its own cheap query.
- **`mark_all_read` is a bulk write**; the client invalidates both the list and the count keys, and does not
  patch items locally.
- The feed is **day-grouped in the UI** with Shamsi headers — a client-side transform over UTC
  `createdAt`. The server sends no grouping.
- Notifications are raised through a self-committing facade (`DispatchAsync`) that runs **after** the
  originating handler's `CommitAsync` — so a notification never exists for a transaction that rolled back.

## Enums

`type` is a **bare string** on the wire and the vocabulary is open-ended by design — new server-side
notification types must not break an older client. The client models the *payload* as a discriminated
union (`NotificationData`, keyed on a `kind` inside `dataJson`) and treats an unknown `type` as
non-actionable rather than an error.

Consequence for the server: **adding a notification type is safe; changing an existing type's `dataJson`
shape is not.** The deep-link parser keys off the payload, not the type string.

## Open REQs

| REQ | Status | Effect |
| --- | --- | --- |
| REQ-054 | deferred, non-blocking | No web push for new booking requests. The nurse dashboard's 15 s poll remains the only signal. Nothing was built for it |
