Push notifications for Rust apps through each platform's own service, APNs on iOS and macOS, FCM or UnifiedPush on Android, WNS on Windows, UnifiedPush on Linux and Web Push in the browser. The app calls install in main before its toolkit starts, sets one handler and calls register. The handler gets the token for the app's server and every push, including the one that started the app.
use pushups::{Config, Event};
if let Err(error) = pushups::install(Config::new()) {
eprintln!("no push on this target: {error}");
}
pushups::set_handler(|event| match event {
Event::Token(token) => println!("send {token:?} to the server"),
Event::RegistrationFailed(error) => eprintln!("no push token: {error}"),
Event::Message(message) => println!("{} bytes, started the app: {}", message.payload.len(), message.started_app),
Event::MessagesDropped => println!("fetch what was missed from the server"),
});
let _ = pushups::register();The handler runs on the platform's delivery thread, so waking the toolkit is its job. The stream feature adds pushups::events(), the same events as a stream. The encryption feature makes the pushes end-to-end encrypted, with the device's keys in the platform key store.
| Android | iOS | macOS | Windows | Linux | Web | |
|---|---|---|---|---|---|---|
| Dioxus 0.7 | yes. A tap into the running app restarts it unless MainActivity is singleTask, as examples/dioxus sets, and the page can occasionally freeze until dioxus#5916 |
yes up to iOS 26, iOS 27 needs dioxus#5893 |
yes |
panics when its window cannot take focus, as on a locked screen, until dioxus#5916 |
yes |
yes1 |
| Tauri 2 | yes |
yes |
yes |
yes |
yes |
|
| winit 0.31 | needs winit#4752, which examples/winit pins |
yes |
yes |
yes |
yes |
yes1 |
| Leptos 0.8 | yes1 |
|||||
| Yew 0.23 | yes1 |
Dioxus and Tauri run on tao, so their cells cover a tao app. Without the winit fix, a fresh install's first launch can get no FCM token until the app is reopened.
| PR | Why |
|---|---|
| dioxus#5901 | dx builds the iOS app icon from Dioxus.toml, the icon iOS shows on every notification |
| dioxus#5898 | dx bundle --macos fills each icon size from the closest PNG, so notifications show a sharp icon instead of one scaled from the first PNG listed |
| dioxus#4063, dioxus#5693 | dx sets the Android launcher icon, which FCM notifications use when the app names no notification icon |
| dioxus#5591 | dx bundle makes an IPA that Apple's validator accepts, the route from dx to the App Store |
| dioxus#5900 | dx renames its bundled JS when only this crate's JavaScript changes, so a web build stops serving the previous release's copy after an upgrade |
| tao#1364 | tao reports the push token and pushes itself, so a tao app without this crate can receive them |
The crate's Gradle module holds the FCM and UnifiedPush services, and its Rust side uses JNI 0.21, as Dioxus and Tauri do. dx bundles the module through the dioxus feature, a Tauri app adds tauri-plugin-pushups, and any other Gradle project depends on io.github.lucacappelletti94:pushups-android at the crate's version or includes android/ through cargo metadata as examples/winit does. A UnifiedPushConfig sends pushes through the device's UnifiedPush distributor when it has one, and Notification::show presents data-message content without an Activity. A push the background handler answers with a notification goes out with android.priority set to high, since FCM may hold a normal-priority message for minutes and demotes high-priority messages that show no notification.
use pushups::{Context, Message};
pushups::firebase_config!("tests/fixtures/google-services.json");
#[pushups::background_handler]
fn on_push(context: Context<'_>, message: Message) {
let _ = (context, message.payload);
}The crate adds its push methods to whatever app delegate the toolkit installs, while closed-app iOS alert transformation uses a separate [rendering_handler] Rust callback and Swift extension adapter. The bundle needs the push entitlement and, on iOS, the remote-notification background mode, which a dx app sets in Dioxus.toml. A Tauri app gets the iOS entitlement and the background mode from tauri-plugin-pushups and names a macOS entitlements file in tauri.conf.json, as examples/tauri does. With the encryption feature the app and the extension share the keychain access group both targets carry in the keychain-access-groups entitlement, which each Info.plist names to the crate with the PushupsKeychainAccessGroup string key.
[ios.entitlements]
aps-environment = "development"
[macos.entitlements]
"com.apple.developer.aps-environment" = "development"
[background]
remote-notifications = trueThe app gives its VAPID public key and serves pushups::SERVICE_WORKER at /pushups-sw.js, which the dioxus feature has dx do and a Trunk build script writes with pushups::write_service_worker, or sets rust_handler to run its own wasm as the worker, as examples/yew and examples/leptos show. On iOS only home-screen web apps get pushes, and they reach the page when it is next shown (WebKit bug 268797).
use pushups::{Config, VapidPublicKey, WebPushConfig};
let key = VapidPublicKey::from_base64url(
"BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4",
)?;
let config = Config::new().web(WebPushConfig::new(key));
# let _ = config;
# Ok::<(), pushups::Error>(())Token::web_push_subscription gives the endpoint and keys the server sends to, and with the serde feature it serialises as the browser's PushSubscription.toJSON(), which web-push and pywebpush read. UnifiedPush tokens on Android and Linux take the same form.
The crate speaks UnifiedPush over D-Bus to the user's distributor, such as KDE's, and a push to a closed app starts it through D-Bus activation for the background handler. The desktop shows nothing for a push by itself, so the app's rendering_handler calls Notification::show with the notification it returns, and a tap on a notification with data reaches the app as a message. Through the desktop portal, 1.19.4 or later, a tap on a closed app opens it. Without that portal a tap reaches the app only while the process that showed the notification runs. LinuxConfig::app_notifications names the app and its icon on the notifications, otherwise they carry the app id.
use pushups::{Config, LinuxConfig, UnifiedPushConfig, VapidPublicKey};
let key = VapidPublicKey::from_base64url(
"BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4",
)?;
let config = Config::new()
.linux(
LinuxConfig::new("org.example.App")
.app_notifications("Example", "/usr/share/icons/example.png"),
)
.unified_push(UnifiedPushConfig::new(key));
# let _ = config;
# Ok::<(), pushups::Error>(())The app's installer puts the Windows App SDK 2.5 runtime on the machine, and the app ships Microsoft.WindowsAppRuntime.Bootstrap.dll beside its executable. It gives the Object ID of its Entra ID app registration's service principal, and a push to a closed app starts it for the background handler. With WnsConfig::app_notifications, naming the app and an icon file, Notification::show puts a toast in the notification centre, and a tap on one with data reaches the app as a message, starting it when it has exited. Windows refuses a toast past about 5 KB, which leaves about 3.5 KB for data under a short title.
use pushups::{Config, WnsConfig};
let config = Config::new().windows(WnsConfig::new("00000000-0000-0000-0000-000000000000"));
# let _ = config;ALTERNATIVES.md compares the other Rust crates that receive pushes, by framework and system.
Footnotes
-
On iOS only home-screen web apps get pushes, and they reach the page when it is next shown (WebKit bug 268797). ↩ ↩2 ↩3 ↩4