nks-talk-notify

Self-hosted push notification gateway from Nextcloud Talk to Android and iOS phones via FCM and APNs. Built for OwnTalk; it cannot read message content.

Preview of nks-talk-notify

nks-talk-notify is a self-hosted push notification gateway from Nextcloud Talk to Android and iOS phones. Anyone developing their own Nextcloud Talk client runs into notifications sooner or later. The app works and messages arrive while it is open, but once the system suspends it, the phone goes quiet. On iOS this happens within a few seconds. A closed app can only be woken by the Apple Push Notification service (APNs) or Firebase Cloud Messaging (FCM), and for both services the sender needs keys that belong to that particular app. nks-talk-notify holds these keys on your own server and delivers Nextcloud notifications through them. It was built for OwnTalk, a Nextcloud Talk client for Android, iOS and desktop, but any client that speaks the push-v2 protocol can use it.

Why the Nextcloud proxy is not enough

Nextcloud itself does not talk to Apple or Google. The notifications app encrypts each notification for a specific device, signs it with the user's key and sends it to the push proxy address the client chose at registration. Only the proxy holds the access keys for APNs and FCM.

The default proxy, push-notifications.nextcloud.com, is run by Nextcloud GmbH and sends with its own APNs key and its own Firebase project. Apple and Google deliver a message only to a sender whose keys belong to the receiving app, so this proxy reaches only the official apps. A third-party app with its own identifier and its own Firebase project needs its own proxy.

What the gateway gives the operator

Notifications for your own app on both platforms

On iOS it sends notifications with the mutable-content flag, so the app extension decrypts them before they are shown. An incoming call is delivered through PushKit if the device registered a VoIP token; otherwise it arrives as a regular notification. A silent message can withdraw a notification. Android receives a data-only message with the priority taken from Nextcloud, so the system never displays the encrypted text by itself.

One gateway for several servers

The gateway accepts notifications from every Nextcloud whose subscription key it knows. The configuration can hold several keys, so several servers can share one instance, such as a company and a family Nextcloud, or the servers of several customers.

Message content stays unreadable

The notification text is encrypted with a key that exists only on the phone. The gateway passes it on but cannot read it.

One process and one file

It is a single Python 3.12 process with the httpx and cryptography libraries and one SQLite file. The Docker image runs as an unprivileged user with a health check; GET /health returns the number of registered devices. Replacing the APNs key or the Firebase service account means restarting the gateway; clients do not need to change anything.

How a message travels

  1. The app registers with Nextcloud and gives the gateway's public address as the proxy address. Nextcloud receives only the SHA-512 hash of the push token, not the token itself.
  2. The app sends the actual token to the gateway at POST /devices together with a signature issued by Nextcloud. The gateway verifies the signature and stores the user's public key.
  3. For a new notification, Nextcloud sends text encrypted with the device key and signed with the user key to POST /notifications. The gateway verifies the signature against the stored key and hands the message to APNs or FCM.
  4. The app extension on the phone decrypts the text and only then shows it. If decryption fails, the user sees the generic title "Nextcloud Talk".

Nextcloud deletes from its records any device the gateway reports as unknown. The gateway therefore marks a device that way only if it has never seen it, or if Apple or Google report that the token has expired.

Where the gateway fits

  • A developer of their own Nextcloud Talk client who has their own Apple Developer account and Firebase project and needs to deliver notifications to a closed app.
  • An administrator of a company Nextcloud whose people use OwnTalk: they enable the gateway with an occ command and put the subscription key into its configuration, and notifications then go through a server under their control.

What the gateway sees and stores

For each device the SQLite file holds its identifier, the user's public key, the push token and its hash, the provider, the APNs environment, a PushKit token if any, and registration times. The device identifier is a SHA-512 hash that Nextcloud computes from the account and session; the gateway never sees the inputs. It does not store user names, server addresses, IP addresses or message metadata. In transit it sees which device receives a notification and when, the IP address of the Nextcloud server and of registering phones, and the size of the encrypted text; Apple and Google see the same for everything that passes through them.

The query string never goes to the log, and logging of the APNs and FCM clients is muted, because the APNs URL contains the device token. The database is created with 0600 permissions, because the tokens are stored in it in readable form.

Protection against abuse

Requests from Nextcloud are verified twice: with the subscription key and, for each item, with an RSA signature that only Nextcloud can produce. The first key registered for a device is locked, so registration under someone else's key ends with code 403. After a successful send, the gateway remembers the message signature for five minutes and discards repeated deliveries. The request body is capped at 1 MiB, a batch at 100 items, registrations at 20 requests per minute from one address and notifications at 120. Per hour it reports at most 10 devices as unknown to Nextcloud, so that a wrong configuration or a lost database cannot wipe all registrations on the Nextcloud side at once.

nks-talk-notify: test suite run
The test suite run with pytest: 129 tests passed in under 20 seconds.

Status and limitations

The protocol has been verified against the source code of the notifications app and against a running Nextcloud 34; older versions have not been tested. OwnTalk is the only client tested so far. The repository has 121 tests that work with generated keys and fake provider responses, so they do not contact Apple or Google.

The gateway is one process with one file and does not scale horizontally. It has no retry queue: a message that APNs or FCM rejects is reported to Nextcloud as failed, and Nextcloud does not resend it. Someone who learns a device identifier before its owner could register it first and block the real device; that, however, requires values from inside the Nextcloud server. The push-v2 format has no nonce or timestamp, so the five-minute signature memory limits replays but does not rule them out entirely; the Nextcloud GmbH proxy is in the same position.

A server can name only one proxy as subscription_aware_server. Once it names this gateway, it stops sending its enterprise subscription key to the Nextcloud GmbH proxy, which affects large installations that also use the official apps.

Getting started

The gateway needs a reverse proxy in front of it with a valid certificate, because Nextcloud accepts only an https:// address. For iOS you need an Apple Developer account, an APNs key and an app identifier; for Android a Firebase project and a service account key. One provider is enough.

cp .env.example .env
$EDITOR .env                  # key ids, topic, host paths of the key files
mkdir -p data && chown 10001:10001 data   # the container runs as uid 10001
docker compose up -d --build
curl http://127.0.0.1:8080/health

On the Nextcloud side, all you need is the notifications app, which comes with the installation, and a working cron. The administrator sets the gateway address:

occ config:app:set notifications subscription_aware_server --value="https://push.example.com"

The subscription key that Nextcloud generates on the first send is read with occ config:app:get notifications push_subscription_key and put into the gateway configuration.

Source code

The code is on GitHub in the nks-hub/nks-talk-notify repository under the GPL-3.0-or-later licence, the same as OwnTalk. The repository has no releases and no prebuilt Docker image; the image is built from the included Dockerfile.

← Back to overview