# Feature COMMS-1078 - Notify desktop users of unread notifications through end-to-end encrypted browser push

Project: Stream Communicator
Status: needs review
URL: https://community.openproject.org/work_packages/COMMS-1078

## Planning

| Attribute | Value |
| --- | --- |
| Date | 2026-10-27 |
| Priority | Medium |
| Target versions | Behind feature flag, 18.1.0, Passion time |
| Sprint | Sprint 20 |
| Backlog bucket |  |
| Votes |  |
| List |  |
| Roadmap |  |
| Relevance |  |

## OPEN POINTS / FEEDBACK

_No work packages_

## Features and Chunks

_No work packages_

## Bugs and other work packages

| ID | Type | Subject | Status | Assignee | Target versions | Spent time | Module |
| --- | --- | --- | --- | --- | --- | --- | --- |
| COMMS-1079 | Task | Document browser push notifications for administrators and users | in progress | Kabiru Mwenja | Behind feature flag, 18.1.0 |  |  |

## Technical Details

| Attribute | Value |
| --- | --- |
| Module |  |
| Feature Flag |  |
| Requires doc change | No |
| Enterprise plan |  |

## Details

| Attribute | Value |
| --- | --- |
| Pull preview |  |
| Mockups | https://www.figma.com/design/WrrK222cxg8hCkco4zpo4i/PWA--Progressive-Web-App-?node-id=0-1&t=htZ5pokw6gky5G3a-1 |
| Accessibility | No |

## People

| Attribute | Value |
| --- | --- |
| Assignee |  |
| Accountable |  |
| Product manager |  |
| Developers | Kabiru Mwenja |
| Designer |  |
| Ordered by |  |
| Signed off by |  |
| Interest by |  |

## Estimates and progress

| Attribute | Value |
| --- | --- |
| PD (min) |  |
| PD (max) |  |
| Work |  |
| Labor costs |  |

## Description

**As** a person who uses OpenProject as an installed desktop application  
**I want** to be told when new notifications arrive, even while the OpenProject window is closed  
**so that** I don't have to keep the application open to notice that something needs my attention.

**Acceptance criteria**

*   **Opt-in, per user and per device**
    *   A user can enable browser push from their notification settings. The browser's permission prompt appears only after that click, never on page load.
    *   Push is off by default. Enabling it on one device does not enable it on another; the settings page lists the user's subscribed devices and lets them revoke each one.
    *   Pausing reminders also pauses push.
*   **Administrators stay in control**
    *   An administrator can turn browser push off for the whole instance. Doing so deletes every stored subscription.
    *   Signing out, locking or deleting a user deletes that user's subscriptions.
*   **Nothing readable leaves the instance**
    *   A push carries no payload at all, so there is nothing for the browser vendor's push service (Google, Apple, Microsoft, Mozilla) to read or relay beyond an empty wake-up. Because there is no payload, no RFC 8291 encryption is needed and the browser's encryption keys are never stored.
    *   The service worker wakes, fetches the unread count and the latest unread event from the instance itself over the user's session, and builds the notification from that. Content level follows COMMS-48.
    *   Metadata is minimised: low urgency, a short time to live, one collapsing topic per user, and at most one push per user within a debounce window.
*   **What the user sees**
    *   One notification that replaces itself rather than stacking. With a single new event it names the actor and reason and the work package ("Alice mentioned you", "WP-123: Fix login") and clicking it opens that work package; with several it reads "N unread notifications" and opens the notification centre. A push that arrives after everything was read shows a silent "No unread notifications" and clears the badge, since browsers require a visible notification per push.
    *   The application icon's badge shows the same count when the push arrives.
    *   If the session has expired or the instance cannot be reached, the notification falls back to a generic "New activity in OpenProject" with no count.
*   **Supported browsers (desktop)**
    *   Chrome, Edge, Firefox and Safari on macOS, including Safari web apps added to the Dock. Push is delivered while the browser is running, and on macOS Safari also while it is not.
*   **Documented**
    *   An administrator page states exactly what is sent and to whom, lists the push hosts the instance must reach, offers a reachability check per host, and explains that air-gapped instances cannot use browser push.

**Technical notes**

*   **Registers its own bare service worker** (install, activate, kill switch when the flag is off) and adds `push` and `notificationclick` handlers. `pushsubscriptionchange` is not handled; the settings page reconciles the browser's subscription with the server on every visit. The offline page (SE-688) is independent; whichever lands second rebases over the worker file.
*   **Browsers require a visible notification for every push.** A badge-only silent push is not possible: Chrome shows its own generic notification, Firefox and Safari revoke the subscription after a few silent pushes. The badge therefore updates alongside the notification, never instead of it. While the application is closed the badge cannot go down when notifications are read elsewhere; it corrects itself on the next launch.
*   **Vendor push services cannot be avoided.** The browser chooses the push service and the instance must POST to the endpoint it is given. The instance needs outbound access to `fcm.googleapis.com`, `*.notify.windows.com`, `updates.push.services.mozilla.com` and `*.push.apple.com`. These receive the subscription endpoint (a stable per-device identifier), the instance's IP and VAPID public key, and the timing and size of each push. This is the data protection exposure to document for customers; the content itself is never exposed.
*   **Server side.** The `web-push` gem (Pushpad, maintained; implements RFC 8030, 8291, 8292; fits the current `jwt` and `openssl` versions). One VAPID key pair per instance, generated on first use and stored encrypted with a key derived from the secret key base; `OPENPROJECT_WEB__PUSH__VAPID__PRIVATE__KEY` overrides it. An unreadable key pauses delivery and shows a banner on the admin page; "Regenerate key" is an explicit admin action that invalidates every subscription. A `push_subscriptions` table (user, session, endpoint), created UNLOGGED because `sessions` is unlogged and a logged table cannot hold a foreign key to it; the cascade from the session row covers logout, revocation and expiry, and rows do not survive an unclean database restart (users enable push again). No `p256dh`/`auth` keys are stored. Delivery from a GoodJob job, one per user, delayed a minute and deduplicated by enqueue limit; any 4xx other than 429 deletes the subscription, 429 and 5xx retry with backoff and give up quietly. At most 10 subscriptions per user. Endpoints are masked from logs. The VAPID contact claim uses the instance URL, not a personal address.
*   **SSRF.** Subscription endpoints come from the client. Allowlist the push hosts above, require https and pin the resolved public address before posting.
*   **Integration point.** Every notification is created through `Notifications::CreateService`, so a hook there (or on `Notification` creation) covers journals, date alerts and reminders, and inherits the existing per-reason notification settings without re-filtering. Because the unread count is recomputed when the push is sent, notifications replaced within the journal aggregation window do no harm.
*   **Shared with native push.** UnifiedPush endpoints (SE-675) accept the same RFC 8030 messages, and OpenProject Mobile has no push yet (SE-665). Model subscriptions so a native kind can be added later without a second delivery pipeline, and so a user with both the desktop application and the mobile app is not notified twice.

**Permissions and visibility considerations**

*   _To whom is this feature visible?_
    *   Every signed-in user, in every edition, on a supported desktop browser over HTTPS, once the administrator has left browser push enabled.
    *   The instance-wide switch sits with administrators under the existing email and notification settings.
*   _When is it not visible?_
    *   When the administrator has turned browser push off, or the instance cannot reach the push hosts.
    *   Over plain HTTP, in browsers without push support, and while the user has not opted in on that device.
    *   On phones and tablets, which are served by the OpenProject Mobile app.

**Translation considerations**

*   "N unread notifications" / "N ungelesene Benachrichtigungen", pluralised, reusing the notification centre's existing terms.
*   "New activity in OpenProject" / "Neue Aktivität in OpenProject" for the fallback.
*   Settings: "Browser push notifications" / "Browser-Push-Benachrichtigungen", "This device" / "Dieses Gerät", "Revoke" / "Widerrufen".
*   The text a browser shows in its own permission prompt is the browser's and cannot be translated by us.

**Out of scope**

*   **Notification content in pushes** (subjects, project names, actors, per-notification click-through). Revisit only after a data protection review of the content-free version.
*   **Native mobile push** (SE-665) and the UnifiedPush server (SE-675), beyond keeping the subscription model open to them.
*   **Silent badge updates** without a visible notification, which no browser allows for web pages.
*   **Safari Declarative Web Push**, which requires a readable title in the payload and so conflicts with the no-payload design.
*   **Self-hosted push services** for on-premise or air-gapped instances.
