On a self-hosted instance, push notifications work correctly while the iOS PWA is foregrounded and the user's presence is online. However, once the app is backgrounded and presence automatically transitions to offline (the normal/expected behavior after a few seconds), push notifications stop arriving entirely even though the push subscription remains valid and registered.
Steps to reproduce
Install Fluxer as a PWA on iOS via Safari's "Add to Home Screen."
Open the app, grant notification permission, confirm status shows online.
Send a test message from another account/device while the PWA is open to confirm the notification arrives.
Background the PWA (swipe up / leave the app) without force-quitting it.
Wait a few seconds for presence to visibly transition from online to offline (confirmed via another client).
Same bug on a different client — it's gateway-side, not platform-specific
Confirming this on a self-hosted instance (Docker Compose, gateway ghcr.io/fluxerapp/fluxer-gateway:v1, Postgres backend) with a GrapheneOS + Brave (Chromium) PWA — i.e. not iOS-specific. Same symptom exactly: web push works while the app is foregrounded/online, and stops entirely once presence transitions to offline, even though the subscription stays valid and registered.
I isolated where it breaks. Short version: the client/delivery side is fully healthy; the gateway never sends the push for an offline recipient.
Ruled out (all verified working)
A raw VAPID Web Push sent directly to the stored subscription (via the web-push CLI, bypassing the gateway entirely) is delivered to the device instantly, even with the browser fully closed. So the VAPID keypair, the stored subscription, FCM transport, and OS-level delivery are all fine.
The gateway's only push-related config is VAPID (which is all standard Web Push needs, and is the same path the manual send used successfully).
Gateway-internal evidence
Gathered via the release's bounded rpc against the live node (bin/fluxer_gateway.real rpc <Mod> <Fun> <Args>, with FLUXER_ERLANG_NODE_NAME / FLUXER_ERLANG_COOKIE / FLUXER_ERLANG_DIST_PORT set):
push:get_cache_stats() → #{push_subscriptions_size => 0, user_guild_settings_size => 4, badge_counts_size => 0, blocked_ids_size => 0} — the guild-settings sync path populates, but push subscriptions never do.
push_token_cache:get(UserId) → undefined.
push_dispatcher:stats() → #{queued => 0, inflight => 0} — even immediately after sending both a DM and a guild @mention to the offline recipient. Nothing is ever enqueued.
Where it breaks
A message to an offline recipient never reaches push_subscriptions:fetch_and_send_subscriptions/8 — the dispatcher stays empty. So the break is upstream of any subscription fetch/send, in the push:handle_message_create/1 → push_eligibility:is_eligible_for_push/8 path.
Note the module surface: there are sync_user_guild_settings / sync_user_blocked_ids paths (which work — hence user_guild_settings_size => 4), but there is no load/sync for push subscriptions, only invalidate_user_subscriptions. That's consistent with subscriptions being fetched on-demand at send time — which means the eligibility/dispatch step simply never fires for offline recipients, so the fetch never happens.
Happy to run more targeted rpc probes on the live node if a maintainer can point at which eligibility branch or dispatch call to instrument.
(Investigation and diagnosis performed with Claude Code.)
For anyone landing here via the native Android app (FCM) rather than a web-push PWA: there is a separate silent-drop bug on that path — the gateway sends an invalid group field in message.android.notification, so FCM rejects every message with HTTP 400 (and push_fcm swallows non-2xx with no log). Details + fix: #745. This issue (web-push / VAPID, offline) appears to be distinct — the web-push payload builder does not hit that field.
2 comments
Comment by @chowe99
ghcr.io/fluxerapp/fluxer-gateway:v1, Postgres backend) with a GrapheneOS + Brave (Chromium) PWA — i.e. not iOS-specific. Same symptom exactly: web push works while the app is foregrounded/online, and stops entirely once presence transitions to offline, even though the subscription stays valid and registered. I isolated where it breaks. Short version: the client/delivery side is fully healthy; the gateway never sends the push for an offline recipient.Ruled out (all verified working)
web-pushCLI, bypassing the gateway entirely) is delivered to the device instantly, even with the browser fully closed. So the VAPID keypair, the stored subscription, FCM transport, and OS-level delivery are all fine.Gateway-internal evidence
Gathered via the release's bounded rpc against the live node (bin/fluxer_gateway.real rpc <Mod> <Fun> <Args>, withFLUXER_ERLANG_NODE_NAME/FLUXER_ERLANG_COOKIE/FLUXER_ERLANG_DIST_PORTset):presence_manager:lookup(UserId)→{error, not_found}.push:get_cache_stats()→#{push_subscriptions_size => 0, user_guild_settings_size => 4, badge_counts_size => 0, blocked_ids_size => 0}— the guild-settings sync path populates, but push subscriptions never do.push_token_cache:get(UserId)→undefined.push_dispatcher:stats()→#{queued => 0, inflight => 0}— even immediately after sending both a DM and a guild@mentionto the offline recipient. Nothing is ever enqueued.Where it breaks
A message to an offline recipient never reachespush_subscriptions:fetch_and_send_subscriptions/8— the dispatcher stays empty. So the break is upstream of any subscription fetch/send, in thepush:handle_message_create/1→push_eligibility:is_eligible_for_push/8path. Note the module surface: there aresync_user_guild_settings/sync_user_blocked_idspaths (which work — henceuser_guild_settings_size => 4), but there is no load/sync for push subscriptions, onlyinvalidate_user_subscriptions. That's consistent with subscriptions being fetched on-demand at send time — which means the eligibility/dispatch step simply never fires for offline recipients, so the fetch never happens. Happy to run more targeted rpc probes on the live node if a maintainer can point at which eligibility branch or dispatch call to instrument. (Investigation and diagnosis performed with Claude Code.)Comment by @cproudlock
groupfield inmessage.android.notification, so FCM rejects every message with HTTP 400 (andpush_fcmswallows non-2xx with no log). Details + fix: #745. This issue (web-push / VAPID, offline) appears to be distinct — the web-push payload builder does not hit that field.Deleted comment