--- title: Notification tags: [data-model, mongoose] aliases: [User Notification, INotification] --- # Notification > **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)) Per-user notification entry. Each row binds to one `userId` (stored as a string rather than ObjectId), carries a typed severity (`info` / `success` / `warning` / `error`) and a domain category, optionally references another entity via `relatedId`, and supports an `actionUrl` for deep-linking. Old notifications are auto-purged by a 90-day TTL index (`createdAt` with `expireAfterSeconds = 7,776,000`). > [!note] Source > `backend/src/models/Notification.ts:18` — schema definition > `backend/src/models/Notification.ts:79` — model export > [!warning] String userId > `userId` is a String, not an ObjectId, and is not declared with `ref`. Consumers must cast to `ObjectId` if they want to `populate()` it as a [[User]]. > [!warning] `category` enum vs reality > The schema enum is `purchase_request` / `offer` / `payment` / `delivery` / `system`, but in practice: > - `notificationController.createNotification` defaults the category to **`'general'`** (`category = 'general'`) when the caller omits it. `'general'` is **not** in the schema enum — Mongoose enum validation will reject it on a strict save, so callers must supply a valid value or the write fails. Treat `'general'` as a value you may encounter in payloads even though it is not an enum member. > - The frontend socket hook `use-notifications.ts` hardcodes `category: 'system'` for every realtime-injected notification, so most client-side notifications surface as `'system'` regardless of their true domain. > - `NotificationService.notifyRequestStatusChanged` always writes `category: 'system'` for purchase-request status changes. ## Schema | Field | Type | Required | Default | Validation | Index | Description | | --- | --- | --- | --- | --- | --- | --- | | `userId` | String | yes | — | — | yes (single + compound) | Owner of the notification. | | `title` | String | yes | — | maxlength 200 | — | Headline. | | `message` | String | yes | — | maxlength 1000 | — | Body. | | `type` | String | yes | `info` | enum: `info` / `success` / `warning` / `error` | — | Severity. | | `category` | String | yes | — | enum: `purchase_request` / `offer` / `payment` / `delivery` / `system` | yes (compound) | Domain bucket. ⚠️ `notificationController` defaults to `'general'` (not in the enum) and the realtime socket hook + `notifyRequestStatusChanged` hardcode `'system'`. See warning above. | | `relatedId` | String | no | — | — | yes | Id of the related entity (e.g. [[PurchaseRequest]]). | | `metadata` | Mixed | no | — | — | — | Arbitrary payload. | | `actionUrl` | String | no | — | maxlength 500 | — | Deep link. | | `isRead` | Boolean | no | `false` | — | yes (compound) | Read flag. | | `readAt` | Date | no | — | — | — | When read. | | `createdAt` | Date | auto | — | — | yes (compound + TTL) | Mongoose timestamp. Auto-deleted after 90 days by TTL index. | | `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. | The collection name is overridden to `notifications` via `collection: 'notifications'`. ## Virtuals None defined. ## Indexes Defined at `backend/src/models/Notification.ts:71-77`: - `{ userId: 1, createdAt: -1 }` — user feed. - `{ userId: 1, isRead: 1 }` — unread badge. - `{ userId: 1, category: 1 }` — category filter. - `{ relatedId: 1 }` — lookup by linked entity. - `{ createdAt: 1 }` with `expireAfterSeconds: 60 * 60 * 24 * 90` (7,776,000 s) — MongoDB TTL index; the database hard-deletes documents automatically after 90 days. Plus the implicit index from `userId` having `index: true` at the field level. ## Pre/Post Hooks None declared. ## Instance Methods None defined. ## Static Methods None defined. ## Status-change notification coverage `NotificationService.notifyRequestStatusChanged` maps a [[PurchaseRequest]] status to a human label via an internal `statusMessages` table. That table covers `pending`, `active`, `received_offers`, `in_negotiation`, `payment`, `processing`, `delivery`, `delivered`, `confirming`, `completed`, and `cancelled`. > [!warning] Missing status templates > The `pending_payment` and `seller_paid` [[PurchaseRequest]] statuses have **no entry** in the `statusMessages` table and no dedicated notification template. Transitions into these states do not produce a meaningful status-change notification (the label falls back to the raw status string, and several flows skip notification entirely). If you rely on notifications for `pending_payment` / `seller_paid`, they will not arrive as expected. ## Relationships - **References**: [[User]] indirectly through `userId` (string); arbitrary entity via `relatedId`. - **Referenced by**: none. ## State Transitions ```mermaid stateDiagram-v2 [*] --> unread unread --> read : user opens read --> [*] : TTL purge (90d) unread --> [*] : TTL purge (90d) ``` ## Common Queries ```ts // User feed Notification.find({ userId }).sort({ createdAt: -1 }).limit(50); // Unread badge count Notification.countDocuments({ userId, isRead: false }); // Mark all read Notification.updateMany( { userId, isRead: false }, { $set: { isRead: true, readAt: new Date() } } ); // All notifications about a request Notification.find({ relatedId: purchaseRequestId.toString() }); ``` Related: [[User]], [[PurchaseRequest]], [[SellerOffer]], [[Payment]].