Push notifications for decentralized services
How mobile push notifications currently bring centralization to decentralized services, and how we can avoid it, even for mainstream configurations.
Published the 31/01/2025
This post will focus on Android.
We all know decentralized applications: emails, Mastodon (and the Fediverse), Nextcloud and other clouds, Git servers, Messaging services like XMPP or Matrix, Feed reader (RSS), some calendars, etc.
They are decentralized because they don’t rely on a single server, own by a single entity. They can be installed by different persons or organizations, on different servers. Mobile applications for these services usually ask the users what server they want to use, before interacting with the service.
Push notifications refer to a mechanism used by servers to inform (mobile) clients of real-time updates. Without push notifications, clients must either periodically request updates from the server, or maintain a connection with their server, which can lead to increased data usage and battery drain.
When Android application developers want to add support for push notifications, they usually go for Firebase Cloud Messaging (FCM), the solution provided by Google.
The guide provided by firebase explain the different steps for you to support their solution:
- You first need to go on the Firebase Console, create a new project and add your application to this project.
You are also strongly recommended to enable analytics for your project.
Google strongly recommend enabling their Analytics
- Then you will need to download a file (json) containing your firebase project information and add it to your application sources.
- Add firebase-messaging to your application, the library that will register to the Google Services on the device (if present).
- Then find out where you can create a new service account for FCM and generate its keys. Most of the time, these keys are considered sensitive, you must ensure to keep them secret. So you must not share them.*
* There might be solutions to generate keys that can be public but it seems easy to mess with this configuration.
When your application server is centralized, you implement the FCM API, and save these keys with your configuration. When you want to inform users about an update, your server will request Google servers with these credentials.
This solution works only for users with Google Services. This is the case for most users but the ones without these services (because Google, or Internet isn’t accessible or because users choose to not install them), you may need another solution, a fallback.
Developers of decentralized services have responded to this expectation of a centralized architecture with gateways:
- The project hosts on a centralized server a service that will “translate” requests from the different servers to Google servers.
- The application server implement their own protocol or follows the standard webpush specifications (RFC8030 + RFC8291 + RFC8292).
- When the server wants to inform the users, it pushes a message with this protocol to the gateway. The gateway knows the Google keys for the application and uses that secret to push to Google servers.
For example:
- Element uses a sygnal server hosted at https://matrix.org/.
- Mastodon uses a webpush-fcm-relay server hosted at https://app.joinmastodon.org/.
- Nextcloud applications use a push proxy (documented here) server hosted at https://push-notifications.nextcloud.com.
The Push gateway is under the (mobile) app developer responsibility
In other words, the applications of decentralized services often respond to this problem by reintroducing a certain centralization. And this still doesn’t solve problems when Google or the Internet is not accessible, or when the user hasn’t installed Google services.
Another often adopted solution is to periodically fetch new events with the server, or to maintain a constant connection to the server. The first solution is good for an application that does not need “real-time” events. And the second can be adapted for the applications that we use intensively, if the use of data and energy for this connection is negligible compared to the basic use of the application. Maintaining a connection can also be very expensive for applications which have not been developed with this use in account. What often happens when the project implements them as a best effort for a small user base.
There is of course the solution of UnifiedPush, that uses webpush specifications, which is decentralized by default and can do authorization controls using the standard VAPID, based an asymmetric encryption.
But as the majority of users rely on Google services, this is understandable that projects target these users first. Some mobile app project have chosen to rely uniquely on UnifiedPush, to simplify things and because users with Google services can install gCompat-UP to bring UnifiedPush support to these services.
Obviously, we know that everybody won’t migrate from a day to the next to UnifiedPush, and we don’t want to force everybody to migrate. But users should be free to use the services of their choice.
What is less known, and not (well?) documented by Google, is that you can directly send webpush requests to FCM servers.
If your application server support Webpush* with VAPID, you can get rid of push gateways. And this can be done by supporting UnifiedPush in the same way:
embedded-fcm-distributor is a library that will register to Google services for webpush registrations, authenticated with VAPID key. It gives support for UnifiedPush to users with Google Services.
To use it, you follow the UnifiedPush main setup. A quick summarize:
You first implement the push service:
class PushServiceImpl : PushService() {
override fun onMessage(message: PushMessage, instance: String) {
processMessage(PushMessage.content)
}
override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) {
sendEndpointToServer(endpoint)
}
override fun onUnregistered(instance: String) {
sendEndpointToServer(null)
}
override fun onRegistrationFailed(reason: FailedReason, instance: String) {
processFail(reason)
}
And subscribe with the server VAPID public key:
UnifiedPush.tryUseCurrentOrDefaultDistributor(context) { success ->
if (success) {
UnifiedPush.register(context, messageForDistributor, vapid)
}
}
It will call the service onNewEndpoint
. Details for the main setup are provided in the library documentation.
Note that it is also possible to upload your VAPID keys on the Firebase Console, if you use a centralized application and want to pushes with Webpush. But being able to register a VAPID pubkey retrieved during execution is not possible with the firebase-messaging library.
* The published version of the standard. There are some applications that rely on a draft of the specifications.
This gives another reason to embrace the standards and implement webpush (and UnifiedPush) !
Mastodon 4.4 will follow the standard webpush specifications. JMAP is an email standard that support webpush, we can have an RFC to add webpush support to IMAP. A proposal to add webpush to Matrix specifications is opened.
We are looking for other decentralized, and centralized, services to support it too. Maybe we’ll lose these gateways one day.
Regarding iOS, they have made some progress in adopting webpush, but it does not seem possible to push for mobile applications with it yet.