Skip to content

feat(push): encrypted push notifications via event_id_only and decryption relay#295

Merged
Just-Insane merged 6 commits intodevfrom
feat/encrypted-push
Mar 16, 2026
Merged

feat(push): encrypted push notifications via event_id_only and decryption relay#295
Just-Insane merged 6 commits intodevfrom
feat/encrypted-push

Conversation

@Just-Insane
Copy link
Collaborator

@Just-Insane Just-Insane commented Mar 16, 2026

What

Implements privacy-first push notifications using the Matrix event_id_only pusher format, with client-side decryption relay for E2EE rooms.

Why

Currently, even with "show encrypted message content" disabled, the homeserver sends the full encrypted event blob + sender display name + room name to Sygnal. With event_id_only, Sygnal sees nothing about message content, sender, or room — it only forwards { room_id, event_id }.

How it works

  1. Pusher registration now sets format: 'event_id_only' — the homeserver strips all content before forwarding to Sygnal.
  2. Service worker detects the minimal payload (room_id + event_id, no type) and fetches the raw event from the homeserver using the stored bearer token. Room name and sender display name are also resolved via homeserver state APIs.
  3. Unencrypted rooms: event content is available immediately — notification shows real sender name, room name, and message body.
  4. Encrypted rooms (m.room.encrypted): SW posts decryptPushEvent to an open app tab and waits up to 5s for a pushDecryptResult reply. The reply includes document.visibilityState so the SW can suppress the OS notification entirely if the app is currently visible (avoiding a double-alert).
  5. App-side (HandleDecryptPushEvent): calls mx.decryptEventIfNeeded() with the raw event, resolves the sender display name and room name via the SDK, and replies with the decrypted content.
  6. Session persistence: the Matrix session (access token, homeserver URL, user ID) is persisted to Cache Storage so that iOS — which kills and restarts the SW fresh for every push event — can still fetch the raw event.

Fallback matrix

Scenario Result
Unencrypted room, any state Full content ✅
Encrypted room, app tab open + visible No OS notification (in-app UI handles it) ✅
Encrypted room, app tab open (Android/desktop) Full decrypted content after relay ✅
Encrypted room, iOS app backgrounded "Encrypted message" ⚠️ — iOS freezes the tab JS before the relay can respond
App fully closed "New Message" (still navigates to room on tap) ⚠️

iOS encrypted message limitation

On iOS, the system kills and restarts the service worker for every push, and any backgrounded app tabs are frozen (JS suspended). The decryption relay (SW → app tab → SW) requires a live JS context to call decryptEventIfNeeded(), which iOS does not provide. As a result, encrypted message content is not shown on iOS — the notification correctly displays sender name and room name, but the body falls back to "Encrypted message".

This is a platform limitation of iOS PWAs. A future native app (e.g. via Tauri v2 + tauri-plugin-push-notifications) could address this using an APNS Notification Service Extension to decrypt in a sandboxed Rust process before the notification is displayed.

Testing

After deploying, re-enable push notifications in Settings to re-register the pusher with the new format field. Then send messages from another client while Sable is backgrounded.

@Just-Insane Just-Insane marked this pull request as ready for review March 16, 2026 14:01
@Just-Insane Just-Insane requested a review from a team March 16, 2026 14:01
@github-actions
Copy link
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

Status Preview URL Commit Alias Updated (UTC)
✅ Deployment successful! https://pr-295-sable.raspy-dream-bb1d.workers.dev 05e1d81 pr-295 Mon, 16 Mar 2026 14:17:54 GMT

@Just-Insane
Copy link
Collaborator Author

Tested push notifications for encrypted rooms in Chrome (desktop - using mobile dev tools option), and it works as expected:

image

@Just-Insane Just-Insane added this pull request to the merge queue Mar 16, 2026
Merged via the queue into dev with commit d1ead29 Mar 16, 2026
7 checks passed
@Just-Insane Just-Insane deleted the feat/encrypted-push branch March 16, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant