Implementing App notifications: The FOSS way

Full Stack Developer | Pythonista | Student | Programmer | Tech Enthusiast | Learner | Linux 🖤
Search for a command to run...

Full Stack Developer | Pythonista | Student | Programmer | Tech Enthusiast | Learner | Linux 🖤
No comments yet. Be the first to comment.
As AI continues to evolve, one of the limitations of generative AI is its tendency to hallucinate—producing inaccurate or nonsensical outputs, especially when it lacks relevant context. This is where Retrieval-Augmented Generation (RAG) comes in. RAG...

The general perception is that deploying applications on the cloud is complicated, but that is not always true. We have really easy ways to deploy stuff on the cloud as well. So I will be demonstrating how to deploy Django Webpps to Azure easily. Pr...

Linux is known for its flexibility and powerful command-line tools. To make the most out of your Linux system, here are five productivity tools that can enhance your workflow. 1) Zoxide (z) So you might be wondering what exactly is zoxide ? According...

In the last blog, we discussed how can we use Django templating to the fullest. In this blog, we will discuss how can we use databases and handle our data in Django. Introduction to Django databases I am hoping everyone knows what a database is, but ...

Recently, I embarked on building a Splitwise-like expense-splitting app using React Native and Expo. This wasn't just another side project—it was my first proper dive into this tech stack, with React Native for mobile development and Golang powering the backend.
From the start, I had one clear goal: avoid third-party dependencies wherever possible. No BaaS solutions like Supabase, Appwrite, or Firebase. I wanted to build everything myself to truly understand the underlying technologies.
This philosophy led me down some interesting rabbit holes. I even implemented native Google Login without relying on any auth provider service—quite challenging since Expo's official Documentation's free solution was being deprecated, and the new approach was locked behind a SaaS paywall. But I persevered and found a way to make it work using a package that used the native credential manager APIs and worked great.
Then came the next feature: push notifications.
A quick Google search seemed to confirm what every developer "knows"—Firebase Cloud Messaging (FCM) is the solution for mobile notifications. FCM appeared to be the only viable option.
Reluctantly, I integrated FCM into both my app and backend. It worked.
Everything changed when I decided to release my app on F-Droid—the open-source Android app marketplace.
That's when I discovered F-Droid's strict policy: apps must be completely open source and cannot depend on any proprietary services. My FCM integration meant I will not be able to release my app on F-Droid.
But this sparked a crucial question: If other apps on F-Droid support notifications, there must be alternatives to FCM, right?
This question sent me deep into research mode. And that's when I stumbled upon something revolutionary: UnifiedPush.
UnifiedPush is a decentralized push notification system that lets you choose the service you want to use. It's designed to be privacy-friendly, flexible, and open—making it perfect if you want control over your push notifications.
Instead of forcing all notifications through a single company's servers (like FCM), UnifiedPush lets users choose their notification provider. This was a paradigm shift I hadn't considered.
To truly grasp UnifiedPush, I needed to understand how notifications actually work. This led me to discover the open standards that power web notifications:
This is a W3C standard that defines how push notifications work on the web. It's the foundation that browsers use for notifications.
WebPush is defined by 3 standards: for the transport (RFC8030), for the authorization (VAPID, RFC8292), for the encryption (RFC8291)
Think of this as a way for your server to prove its identity when sending notifications. Instead of using proprietary API keys tied to specific services, you generate your own cryptographic key pair:
Public key: Identifies your app
Private key: Signs your messages
What's beautiful about this approach is that it's service-agnostic. The same VAPID keys and WebPush messages work whether the user chooses Google's servers, a self-hosted solution, or anything in between.
Privacy by Design: Here's a crucial security feature that sets webpush apart—encryption. When your server sends a notification, the message is encrypted using the user webpush keys. This means:
✅ Only your app can decrypt the notification content
✅ Distributors (ntfy, NextPush, etc.) cannot read your messages
✅ Even if using FCM as a distributor, Google cannot see your data
✅ True privacy regardless of which distributor the user chooses
This is fundamentally different from traditional FCM, where Google's servers can potentially access all notification content. With UnifiedPush's VAPID encryption, the distributor is just a secure messenger—they can deliver your encrypted package, but they can't open it.
Here's the key insight: mobile devices can receive WebPush notifications too. The same standards that power web notifications work on mobile platforms.
UnifiedPush elegantly bridges the gap between open standards and mobile notifications:
Step 1: App Sends Notification
Your server sends a WebPush message to the user's chosen distributor
Step 2: Distributor Delivers Notification
The distributor receives the message and forwards it to your app
Step 3: Device Receives Notification
Your app wakes up and processes the data, showing a notification to the user
Distributors (or Notification Providers) are apps that run in the background and handle notification delivery. Users can choose from various options:
ntfy: Self-hosted, maximum privacy
Sunup: Uses Mozilla's Push Servers
gCompat-UP: Uses Google's infrastructure as a fallback
Custom solutions: Roll your own

Now, you might be thinking: "This sounds great, but requiring users to install another app seems like a UX nightmare. Users will have to install another app"
Here's where UnifiedPush gets really clever: embedded distributor modules.
UnifiedPush provides an embedded distributor that your app can use internally. When using this embedded module:
Your app registers for notifications directly with FCM endpoints
No Firebase project or configuration required
No Firebase libraries needed
FCM natively supports WebPush and VAPID standards
This means you can send notifications to your app using direct FCM endpoints without any Firebase setup—just pure WebPush with VAPID authentication.
My implementation strategy became clear:
1. Check if user has any external distributors installed
2. If yes → Give them the option to choose their preferred distributor
3. If no → Use the embedded distributor seamlessly
This approach provides:
Zero UX friction for mainstream users
Full choice and privacy for power users
F-Droid compatibility
The backend migration was surprisingly straightforward. I simply replaced the Firebase library with a standard web-push library. Instead of dealing with Firebase service accounts and complex authentication, I just needed:
Generate VAPID key pairs
Use WebPush protocol for sending notifications
Handle subscription management
On the React Native side, replacing FCM with UnifiedPush was not exactly smooth for me. The expo-unified-push library provided a cleaner API than Firebase's implementation, with better debugging capabilities and more predictable behavior.
However, I hit a frustrating wall.
The Problem: Notifications worked perfectly when my app was in the foreground, but completely failed when the app was closed or in the background. This is obviously a deal-breaker for any notification system.
My Desperate Debugging Journey:
Spent countless hours debugging the UnifiedPush library source code
Tried different distributors, thinking it was distributor-specific
Even forked the expo-unified-push repository and made custom modifications
Used my own version of the library as the npm package instead
Tested various permission configurations and background processing settings

Me debugging for hours
The False Victory: My forked version seemed to work initially, which made me think I'd solved some complex architectural issue. I was ready to write a detailed post about the "fixes" needed for background notifications.
The Humbling Reality: The next day, after seeing the developer's comments, I discovered the real culprit. It wasn't the library, the distributors, or any complex background processing limitation.
The actual issue: The notification id field format.
I had been either:
Omitting the id field entirely, or
Using hexadecimal IDs (like MongoDB ObjectIds or UUIDs)
The solution: Unified push library requires notification IDs to be numeric only.

Me realizing the bug was just using wrong ID format
The Lesson: Sometimes the most frustrating bugs have the simplest solutions. What felt like a fundamental architectural problem was actually a basic data format requirement that I'd overlooked.
Throughout this debugging nightmare, I wasn't struggling alone. The UnifiedPush community on Matrix was incredibly supportive and patient with my questions.
Even when I was going down the wrong path with my forked library approach, the community helped me think through the problem systematically. They suggested checking payload formats, testing with different distributors, and reviewing the notification ID requirements—which ultimately led me to discover the numeric ID issue.
The human side of open source: This experience reminded me why I love open source communities. Instead of being stuck with corporate support tickets or waiting for Stack Overflow responses, I had direct access to the people building and maintaining the tools I was using. They were invested in my success because my success meant UnifiedPush was working for more developers.
Mainstream users: Zero change in experience—notifications "just work"
Privacy-conscious users: Full control over notification routing
Degoogled device users: Finally have access to full app functionality
Now, let's address the elephant in the room: iOS.
Apple's ecosystem presents unique challenges for alternative notification systems. Unlike Android, where users can install alternative app stores and modify system behavior, iOS maintains strict control over push notifications through APNs (Apple Push Notification service).
The Current iOS Situation:
All iOS notifications must go through Apple's APNs servers
No alternative notification distributors allowed
Users cannot choose their notification provider
Apps distributed outside the App Store (sideloaded) still require APNs for notifications
Apple being apple, they have won't allow any third-party notification systems to replace APNs. They won't wanna give up control over notifications, which is a shame because it limits user choice and privacy.
I will soon be releasing my app on F-Droid (Hopefully)
But more importantly, I've discovered an entire ecosystem of developers building user-centric, privacy-respecting technology. The UnifiedPush community is growing rapidly, with major apps like Element, Tusky, and FluffyChat all adopting this approach.
If you're building mobile apps and care about user privacy, open standards, or F-Droid compatibility, I highly recommend exploring UnifiedPush. The implementation is often simpler than traditional solutions, and your users will appreciate having real choice in how their data flows.
The future of mobile notifications doesn't have to be controlled by Big Tech. We can build something better—something that puts users first.
https://unifiedpush.org/news/20250131_push_for_decentralized/
https://unifiedpush.org/
https://developer.chrome.com/blog/web-push-interop-wins
Want to follow my journey building privacy-first mobile apps? Connect with me or check out my other projects!