Files
eryao/docs/protocols/notification/notification-inbox-protocol.md
T

192 lines
4.6 KiB
Markdown
Raw Normal View History

2026-04-10 18:50:08 +08:00
# Notification Inbox Protocol (Frontend <-> Backend)
This document defines the notification inbox contract for authenticated users.
Protocol verification status:
- Backend route source: `backend/src/v1/notifications/router.py`
- Backend service source: `backend/src/v1/notifications/service.py`
- Backend repository source: `backend/src/v1/notifications/repository.py`
- Backend schema source: `backend/src/v1/notifications/schemas.py`
## Compatibility strategy
- Additive evolution only.
- Existing response fields are stable and must remain backward-compatible.
- New `action` values may be added to `payload`; unknown `action` values must be ignored by the client.
## Routes
### GET /api/v1/notifications
List notifications for the current user.
**Authorization**: Requires authenticated session. User identity from JWT `sub`.
**Query parameters**:
- `limit` (optional, integer, default 20, max 50): number of items per page
- `cursor` (optional, string): pagination cursor (ISO 8601 timestamp of last item's `created_at`)
**Response (200)**:
```json
{
"items": [
{
"id": "uuid",
"notificationId": "uuid",
"type": "system",
"title": "Welcome",
"body": "Welcome to the app!",
"payload": {
"action": "none"
},
"isRead": false,
"readAt": null,
"createdAt": "2026-04-10T00:00:00Z"
}
],
"nextCursor": "2026-04-09T12:00:00Z",
"hasMore": true
}
```
Field rules:
- `items`: array of notification items, ordered by `createdAt` descending
- `nextCursor`: timestamp cursor for next page, `null` if no more items
- `hasMore`: boolean indicating if more items exist
- `type`: string, currently only `system`
- `payload`: discriminated union (see Payload section below)
- `isRead`: boolean
- `readAt`: ISO 8601 timestamp or `null`
- Results are filtered: `notifications.status = 'published'` and `notifications.deleted_at IS NULL`
### GET /api/v1/notifications/unread-count
Get the number of unread notifications for the current user.
**Authorization**: Requires authenticated session. User identity from JWT `sub`.
**Response (200)**:
```json
{
"count": 5
}
```
Field rules:
- `count`: integer `>= 0`
- Counts only notifications where `notifications.status = 'published'` and `notifications.deleted_at IS NULL`
### PATCH /api/v1/notifications/{id}/read
Mark a single notification as read. Idempotent.
**Authorization**: Requires authenticated session. `id` must belong to the current user's `user_notifications`.
**Path parameters**:
- `id`: UUID of the `user_notifications` record
**Response (200)**:
```json
{
"id": "uuid",
"notificationId": "uuid",
"type": "system",
"title": "Welcome",
"body": "Welcome to the app!",
"payload": {
"action": "none"
},
"isRead": true,
"readAt": "2026-04-10T01:00:00Z",
"createdAt": "2026-04-10T00:00:00Z"
}
```
**Error responses**:
- 404 `NOTIFICATION_NOT_FOUND`: notification not found or not owned by current user
- Already-read notifications return 200 with current state (idempotent)
### PATCH /api/v1/notifications/mark-all-read
Mark all unread notifications for the current user as read. Idempotent.
**Authorization**: Requires authenticated session. User identity from JWT `sub`.
**Response (200)**:
```json
{
"updatedCount": 3
}
```
Field rules:
- `updatedCount`: integer `>= 0`, number of notifications that were actually changed from unread to read
- If all notifications are already read, returns `{ "updatedCount": 0 }`
- Only affects notifications where `notifications.status = 'published'` and `notifications.deleted_at IS NULL`
## Payload
`payload` is a discriminated union based on the `action` field.
### action = "none"
No navigation action on tap.
```json
{
"action": "none"
}
```
### action = "open_route"
Navigate to an in-app route.
```json
{
"action": "open_route",
"route": "/divination/history",
"entityId": "optional-uuid",
"tab": "optional-tab-name"
}
```
Field rules:
- `route`: required, string, max 200 characters, app-internal route path
- `entityId`: optional, string, max 64 characters, business object ID
- `tab`: optional, string, max 32 characters, sub-page navigation parameter
- `url`: must be absent
### action = "open_url"
Open an external URL.
```json
{
"action": "open_url",
"url": "https://example.com/page"
}
```
Field rules:
- `url`: required, string, max 500 characters, external URL
- `route`, `entityId`, `tab`: must be absent
## Error contract linkage
- RFC7807 + extension `code`, optional `params`.
- Shared registry: `docs/protocols/common/http-error-codes.md`.
- New error codes for this feature are registered in the same registry.