Skip to content

Push Notifications — client guide

Partly out of date

Rendered from ProjectInfo/client/push-notifications.md on docs/encryption-modes-spec. Its protocol content is still useful, but parts of it describe an older client or an earlier API. Where it disagrees with the code, the code wins — see the spec errata.

The design, threat model and self-hosting trade are in ../push-notifications.md. This page is the client's checklist. Full request/response detail: Server/docs/api.md §17.

Earlier revisions of this page described POST /api/device-tokens and a push body of "Message from {senderUsername}". Neither exists: the endpoint was never routed, and a sender name in a push hands Google a log of who messages whom. Do not reintroduce either.

The model

The server never sends content. Every push is the same constant wake, {"v":"1","t":"wake"} — no sender, channel, server or count. On a wake:

  1. POST /api/push/drain — returns and clears this device's queue, one entry per channel: channel_id, server_id (null for DMs), is_dm, message_count, first_message_id, last_message_id.
  2. Fetch each channel's messages from first_message_id onwards through the ordinary endpoints, decrypt them with the keys the device already holds.
  3. Decide, locally:
  4. DM or group DM → notify.
  5. Server channel → notify only if it mentions you (<@your-id>) or @everyone. The server cannot see mentions — they are in the ciphertext — so it wakes for every unmuted message and the client filters.
  6. Your own message, or a muted channel/server → nothing.
  7. Draw the notification on the device. Decrypted text never leaves it.

Only one process may decrypt at a time: a DM decrypt advances the Double Ratchet. If the app is running, hand the wake to it rather than decrypting in a second process.

Registering

GET /api/push/config              → {"fcm": bool, "unifiedpush": true}

PUT /api/push/registration
{ "transport": "fcm",         "token": "<FCM registration token>" }
{ "transport": "unifiedpush", "token": "<endpoint URL>",
  "p256dh": "<base64url P-256 point>", "auth": "<base64url 16 bytes>" }
→ 200

DELETE /api/push/registration     → 204

All scoped to the calling device by the token's device id. One registration per device; a new PUT replaces the old one, so switching transport is one call.

  • Choose UnifiedPush when a distributor is installed; else FCM if the build has Firebase config and fcm is true; else no push, and tell the user why (on a self-hosted instance, fcm is normally false — they need a UnifiedPush distributor).
  • Send the UnifiedPush connector's RFC 8291 keys. With them the distributor sees only ciphertext.
  • Re-register on every app start, sign-in and token/endpoint change.
  • On logout, DELETE /api/push/registration before discarding the access token.

Lifecycle the server enforces

A registration is deleted when the transport reports it dead, after repeated failures, when its device is revoked, when the device has no live session (swept every 10 minutes), or after 90 days without a re-registration. A muted channel or server never wakes the device.

Platforms

  • Android: implemented in Client-Flutter/lib/features/push/. See that directory.
  • Web: no push. The open tab has the gateway.
  • iOS / desktop: not built. APNs support was removed from the worker with the unreachable endpoint; an iOS client would need it re-added as another content-free transport.