Skip to content

[RFC]: User Notification System (Browser Push + Email Fallback) #289

Description

@jeromevdl

Summary

Add a multi-channel notification system so users are alerted when something requires their attention — even when they're not actively looking at the app. Notifications escalate through three channels based on user presence: in-app toast (tab visible), browser push notification (tab hidden), and email via SES (app closed). Triggers are @mentions in discussions and agent questions requiring human input.

Motivation

Today, @mention notifications are ephemeral WebSocket toasts. If the user's tab isn't focused or the app is closed, the notification is silently lost. Users have no way to know something needs their attention unless they're actively watching the screen. This is a problem for a collaborative tool where discussions and agent questions can happen asynchronously. The DynamoDB notifications table already exists in infrastructure but is completely unused: there's no persistent inbox, no browser push, and no email fallback

Detailed design

Triggers (when to notify)

Event Recipients
@mention in a discussion The mentioned user(s)
Agent question requiring human input Project owner (+ optionally members, based on preference)

Channel escalation logic

Event occurs
  → Identify recipient(s) (exclude the author)
  → For each recipient:
      ├─ Active WS connection + tab visible  → In-app toast (existing behavior)
      ├─ Active WS connection + tab hidden   → Browser Notification (Notification API)
      └─ No active WS connection             → Email via SES

Presence detection

  • Server-side (email decision): Query connections DynamoDB table by UserIdIndex. If user has at least one connection with a valid (non-expired) token → online (use WebSocket). Otherwise → offline (send email).
  • Client-side (browser notification decision): The frontend WebSocket handler checks document.visibilityState. If 'hidden' → fire new Notification(...). If 'visible' → show toast only.

Backend changes

  1. Activate the notifications table — add TTL attribute (30-day expiry), define item schema:

    PK: userId (S), SK: timestamp (N)
    Attributes: id, type ('mention'|'question'), payload (map), read (bool), deliveredVia ('ws'|'email'|'both')
    
  2. Notification dispatcher Lambda — receives events (from discussion Lambda inline call or via EventBridge), persists to notifications table, checks user presence, routes to WebSocket (broadcastToUser) or SES.

  3. REST endpointsGET /notifications (list, paginated), PATCH /notifications/{id} (mark read), GET /notifications/unread-count, PUT /notifications/preferences.

  4. SES integration — Terraform module for verified domain, email template (subject: [ProjectName] {byName} mentioned you, body: excerpt + deep link + unsubscribe).

  5. User preferences — stored on User vertex or in a new DynamoDB item:

    • emailNotifications: bool (default: true)
    • notifyOnMention: always on
    • notifyOnAgentQuestion: bool (default: true for owners, false for members)

Frontend changes

  1. Browser Notification API — request permission on login, fire new Notification() when WebSocket event arrives and document.visibilityState === 'hidden'.

  2. Notification inbox — bell icon in header with unread badge, dropdown listing recent notifications with "mark as read" and "jump to thread" actions.

  3. Preferences UI — settings page/modal for toggling email and per-event-type notifications.

Architecture diagram

┌────────────────────────────────────────────────────────┐
│  Event Source (discussions Lambda / agent Lambda)      │
└──────────────────────────┬─────────────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────┐
│  Notification Dispatcher Lambda                      │
│  1. Persist to DynamoDB notifications table          │
│  2. Query connections table (user online?)           │
│     ├─ Online  → broadcastToUser (WebSocket)         │
│     └─ Offline → SES SendEmail                       │
└──────────────────────────────────────────────────────┘
                   │                    │
          ┌────────┘                    └────────┐
          ▼                                      ▼
┌──────────────────┐                   ┌──────────────────┐
│   WebSocket      │                   │   Amazon SES     │
│   (existing)     │                   │   (new)          │
└────────┬─────────┘                   └──────────────────┘
         │
         ▼  (frontend)
┌──────────────────────────────────────┐
│ document.visibilityState check       │
│  ├─ 'visible' → in-app toast         │
│  └─ 'hidden'  → Notification API     │
└──────────────────────────────────────┘

Alternatives considered

  1. SNS for push delivery — SNS supports email/SMS subscriptions natively but adds topic management overhead, doesn't support rich HTML emails well, and we'd still need SES for formatting. Using SES directly is simpler for email-only delivery.

  2. Service Worker + Web Push API — enables true push notifications even when the browser is fully closed (no tab at all). Rejected because it requires a push subscription server, VAPID keys, and adds significant complexity. The Notification API (requires at least one tab open) covers the most common case. Can be added later.

  3. Polling-based notification inbox without push — simpler but defeats the purpose. Users would only see notifications when they manually check. The whole point is proactive alerting.

  4. Notify all project members on every discussion message — too noisy. Scoping to @mentions and agent questions keeps signal-to-noise ratio high.

Breaking changes

None. This is purely additive:

  • The notifications DynamoDB table already exists (unused) — activating it doesn't affect existing data.
  • The WebSocket broadcastToUser function already exists — extending its usage doesn't change existing behavior.
  • Browser Notification API requires explicit user permission grant — no automatic behavior change for existing users.
  • Email notifications are opt-out (enabled by default) but can be disabled immediately via preferences.

Open questions

  1. Email batching — if a user is offline and receives 5 mentions in 10 minutes, should we send 5 separate emails or batch into a digest? Proposal: send immediately for the first, then batch subsequent ones in a 5-minute window.

  2. Notification TTL — 30 days? 90 days? Should it be configurable per project?

  3. SES production access — new AWS accounts start in SES sandbox (can only send to verified emails). Need to plan for production access request during deployment. Should we document this or automate it?

  4. Service Worker (v2) — is true offline push (app fully closed) important enough to prioritize for v2, or is the Notification API (requires one tab open) sufficient for foreseeable use cases?

  5. Agent questions targeting — should "agent question" notifications go to all members or just the project owner? Should this be configurable per project?

  6. Slack/Teams integration — should we support a third-party chat channel (Slack, Teams, Discord) as an alternative to email for offline notifications? The simplest approach would be a project-level Incoming Webhook URL (owner configures it in project settings, all notifications for the project go to that channel). Per-user Slack DMs would require a full OAuth app and user mapping — significantly more effort.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions