Notification
Data Entity
Description
Persisted notification records delivered to users via push, email, or SMS channels. Tracks delivery status, read state, and links to triggering domain events for audit and display in the notification inbox.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Primary key — globally unique notification record identifier | PKrequiredunique |
user_id |
uuid |
FK → users.id — the recipient of this notification | required |
organization_id |
uuid |
FK → organizations.id — tenant scope, used to enforce data isolation | required |
channel |
enum |
Delivery channel used for this notification | required |
type |
enum |
Scenario/trigger type that caused this notification to be created | required |
title |
string |
Short notification title shown in inbox and push banner (max 120 chars) | required |
body |
text |
Full notification body text. Rendered in notification inbox detail view. | required |
deep_link |
string |
Optional URI to navigate the app to the relevant screen when notification is tapped (e.g. meander://assignment/abc123) | - |
status |
enum |
Delivery lifecycle status of this notification record | required |
read_at |
datetime |
Timestamp when the user explicitly opened or acknowledged the notification in the inbox. Null = unread. | - |
sent_at |
datetime |
Timestamp when the notification was dispatched to the external provider (FCM, email relay, SMS gateway) | - |
failed_at |
datetime |
Timestamp of terminal delivery failure. Populated alongside failure_reason. | - |
failure_reason |
string |
Human-readable error from the delivery provider when status = failed | - |
reference_type |
string |
Domain entity type the notification relates to (e.g. 'assignment', 'expense', 'event'). Used to resolve deep_link and display contextual detail. | - |
reference_id |
uuid |
ID of the domain entity instance that triggered this notification (e.g. the assignment UUID). Nullable for system announcements. | - |
scenario_id |
string |
Identifier of the Scenario Engine rule that generated this notification. Enables traceability back to scenario configuration. | - |
metadata |
json |
Arbitrary key-value payload passed to the notification template renderer (e.g. peer_mentor_name, threshold_count). Stored as JSONB in PostgreSQL. | - |
priority |
enum |
Delivery priority hint passed to push provider (FCM data vs notification message) | required |
locale |
string |
BCP-47 locale code used to render the notification body (e.g. 'nb', 'en', 'se'). Supports Sami language notifications. | - |
created_at |
datetime |
Record creation timestamp — when the notification was enqueued | required |
expires_at |
datetime |
Optional TTL — notification will not be delivered after this timestamp. Used for time-sensitive alerts (e.g. event reminders). | - |
Database Indexes
idx_notifications_user_id
Columns: user_id
idx_notifications_user_status
Columns: user_id, status
idx_notifications_user_read_at
Columns: user_id, read_at
idx_notifications_organization_id
Columns: organization_id
idx_notifications_type
Columns: type
idx_notifications_reference
Columns: reference_type, reference_id
idx_notifications_created_at
Columns: created_at
idx_notifications_status_pending
Columns: status, created_at
Validation Rules
valid_user_id
error
Validation failed
title_length
error
Validation failed
body_not_empty
error
Validation failed
valid_channel_for_type
error
Validation failed
valid_deep_link_format
error
Validation failed
reference_consistency
error
Validation failed
valid_locale
error
Validation failed
status_transition_validity
error
Validation failed
metadata_is_valid_json
error
Validation failed
Business Rules
respect_user_preferences
Before creating a notification record and dispatching it, the scenario engine must check notification_preferences for the target user. If the user has disabled the relevant notification type or channel, the notification must not be created or sent.
tenant_isolation
Notifications may only be read or modified by users belonging to the same organization_id. API middleware must enforce this; cross-tenant access returns 403.
no_send_after_expiry
If expires_at is set and the current time exceeds it, the notification must be cancelled (status = cancelled) rather than dispatched. Prevents stale reminders for past events.
single_inbox_per_channel
For in_app channel notifications, only one unread notification per (user_id, type, reference_id) combination should be active at a time. Duplicate scenario triggers within 24 hours must be deduplicated to avoid inbox flooding.
assignment_reminder_timing
Assignment reminder notifications (type = assignment_reminder) must be triggered exactly 10 days after an assignment is dispatched if no read_at has been recorded for the corresponding assignment_dispatched notification. Governed by the assignment reminder scheduled job.
high_priority_push_only
Notifications with priority = high (e.g. assignment_dispatched with encrypted data) must always be delivered via push channel regardless of user channel preferences, to ensure timely receipt of sensitive assignments.
mark_read_idempotent
Marking a notification as read is idempotent. If read_at is already set, subsequent read updates must not overwrite the original timestamp.