User Notifications System
Approvedv1.2Owner: @product-team
TL;DR
Unified real-time notification system that replaces email-only alerts with in-app, push, and email delivery across all BeWith touchpoints.
Background
Users currently receive critical updates only via email, leading to delayed responses and missed actions. Support tickets related to "missed updates" account for ~18% of monthly volume.
Problem Statement
Users are missing time-sensitive events because there is no real-time, in-app notification mechanism.
Affected personas:
- End users who need to act on requests
- Admins who monitor platform health
- Integration partners expecting webhook reliability
Goals
- Real-time in-app notifications (< 500ms delivery P95)
- Push notifications for mobile
- User-controlled notification preferences
- Notification history (30-day retention)
- Batch digest emails for low-priority events
Non-Goals
- SMS notifications (deferred to v2)
- Third-party notification aggregators (Slack, Teams) — tracked separately in a follow-up RFC
- Marketing/promotional notifications
Success Metrics
| Metric | Baseline | Target |
|---|---|---|
| Support tickets re: missed updates | 18% of volume | < 5% |
| Notification open rate (in-app) | — | ≥ 70% |
| Delivery P95 latency | — | < 500ms |
| User preference adoption | — | ≥ 60% users configure prefs within 30 days |
User Stories
As an end user,
I want to see a real-time badge in the navbar when I have unread notifications
so that I don't have to check my email to know something needs my attention.
As an admin,
I want to receive a push notification when a critical system alert fires
so that I can respond immediately regardless of whether I have the app open.
As any user,
I want to control which notification types I receive and via which channel
so that I'm not overwhelmed by noise.
Architecture
High-level flow
Notification preference model
Solution Design
Data model
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
type TEXT NOT NULL, -- 'request_approved', 'system_alert', …
priority TEXT NOT NULL DEFAULT 'medium', -- 'high' | 'medium' | 'low'
title TEXT NOT NULL,
body TEXT,
data JSONB, -- arbitrary payload
read_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX notifications_user_unread
ON notifications(user_id, created_at DESC)
WHERE read_at IS NULL;
API
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/notifications | Paginated notification list |
PATCH | /api/v1/notifications/:id/read | Mark single as read |
POST | /api/v1/notifications/read-all | Mark all as read |
GET | /api/v1/notifications/preferences | Get user prefs |
PUT | /api/v1/notifications/preferences | Update user prefs |
Edge Cases
| Case | Handling |
|---|---|
| User offline when notification fires | Persist; deliver via push; show on next app load |
| WebSocket disconnects mid-delivery | Client reconnects and fetches unread on mount |
| User disables all channels | Record persisted; available in history |
| Notification storm (> 50 events/min for one user) | Batch into digest, rate-limit push |
Open Questions
- @eng-lead — Should WebSocket connections be managed by the API Gateway or a dedicated WS service?
- @product-team — What's the retention policy for read notifications? 30 days proposed.
- @design — Notification panel UX — flyout vs dedicated page?
Alternatives Considered
| Option | Pros | Cons | Decision |
|---|---|---|---|
| Polling | Simple | Battery drain, latency | ❌ Rejected |
| Server-Sent Events | Simpler than WS | One-directional | ❌ Rejected |
| WebSockets | Bidirectional, low latency | Connection management | ✅ Chosen |
| Third-party (Knock, Novu) | Quick to ship | Vendor lock-in, cost | Deferred to v2 evaluation |
Milestones
Last updated by @product-team on 2026-05-20