# Notifications

Real-time notification infrastructure with WebSocket delivery and
optional email transport.

**Auth:** All endpoints require authentication (JWT via `sb_token`
cookie or `Authorization: Bearer` header).

### CRUD

```
GET    /notifications                → paginated list
GET    /notifications/unread-count   → { "count": 5 }
PATCH  /notifications/:id/read       → mark one as read
POST   /notifications/mark-all-read  → mark all as read
DELETE /notifications/:id
```

**`GET /notifications`** — paginated list for the authenticated user.

Query parameters:

| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `limit` | int | 20 | 1–100, clamped |
| `offset` | int | 0 | Pagination offset |
| `unread_only` | bool | false | Filter to unread only |

Response:

```json
{
  "data": [ ... ],
  "total": 42,
  "limit": 20,
  "offset": 0
}
```

Notification object:

```json
{
  "id": "uuid",
  "user_id": "uuid",
  "type": "role.fallback",
  "title": "Role Fallback Triggered",
  "body": "Primary model unavailable, using fallback",
  "resource_type": "channel",
  "resource_id": "uuid",
  "is_read": false,
  "created_at": "2025-01-15T12:00:00Z"
}
```

`resource_type` and `resource_id` are optional — used for
click-to-navigate. The resource may be deleted (no FK constraint).

### Preferences

Users control per-type delivery:

```
GET    /notifications/preferences           → { "data": [...] }
PUT    /notifications/preferences/:type     ← { "in_app": true, "email": false }
DELETE /notifications/preferences/:type      → reset to default
```

**`GET /notifications/preferences`** returns all preferences set by
the user. Empty array if none are configured.

**`PUT /notifications/preferences/:type`** creates or updates a
preference. Both `in_app` and `email` fields are optional (merges
with existing values if set).

**`DELETE /notifications/preferences/:type`** removes the preference,
falling back to the next level in the resolution chain. Idempotent —
deleting a non-existent preference returns 200.

Resolution: specific type pref → user wildcard `*` pref → system
default (`in_app=true`, `email=false`).

Returns 503 if the preference store is not available (unmanaged mode).

### Admin

```
POST   /admin/notifications/test-email      → send test email to requesting admin
```

Requires admin role. Loads SMTP config from platform settings,
sends a test message to the admin's registered email address.

### Admin Broadcast (v0.28.6)

```
POST /admin/notifications/broadcast
```

**Auth:** Admin only.

Sends a `system.announcement` notification to all active users via
`NotifyMany`. Also emits a `system.broadcast` WebSocket event for
real-time visibility.

**Request body:**

```json
{
  "title": "Scheduled Maintenance",
  "message": "Servers will be down from 2-4 AM EST.",
  "level": "warning"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | yes | Short summary shown in bell dropdown |
| `message` | string | yes | Detail text |
| `level` | string | no | `info` (default), `warning`, or `critical` |

**Response:**

```json
{ "message": "broadcast sent", "count": 42 }
```

`count` is the number of active users notified.

### WebSocket Events

| Event | Payload | Description |
|-------|---------|-------------|
| `notification.new` | Full notification object | New notification created |
| `notification.read` | `{"id": "uuid"}` | Single notification marked read |
| `notification.read` | `{"action": "mark_all_read"}` | All notifications marked read |
| `system.broadcast` | `{"title", "message", "level"}` | Admin announcement (v0.28.6) |

These are targeted via `Hub.SendToUser` — no room subscription
required. Used for badge sync across tabs.

### Notification Types

See [enums.md](enums.md#notification-types) for the canonical list.
Types are free-form strings (`domain.action` convention) — new types
don't require migration. Core types include `system.announcement`
(admin broadcast, v0.28.6).

### Retention

Background cleanup prunes notifications older than 90 days (default,
configurable via `WithRetention`). Runs daily after a 5-minute
startup delay.

---
