Push Notifications: How Delivery Actually Works

The delivery chain

Every push goes through the same path, and a fault at any link looks identical to the member (nothing arrives). Know the chain before you debug:

  1. Device registers. The installed app asks for notification permission, then obtains a Firebase registration token. A raw APNs token is never stored — the sender cannot use one.
  2. Token is saved to push_tokens against user, gym, platform and a stable per-install device_id. One phone occupies one row; reinstalls replace rather than accumulate.
  3. Something triggers a send — a class cancellation, a cover request, an alert, an appointment, a maintenance fault.
  4. The send-push-notification function resolves the recipients' tokens, respects their per-category notification preferences, and hands the payload to the FCM relay.
  5. The relay (Cloud Run) signs the request with the Firebase service account and calls FCM HTTP v1.
  6. In parallel, persistNotifications writes the same message in-app, so the bell and Activity list show it even if the device push fails.

Deep links

Payloads carry a route (historically url or deep_link). Tapping stores the route and navigates once the session is ready, so a cold start launched from a notification still lands in the right place. Only /member and /dashboard routes are accepted — anything else is ignored by design.

Preferences

Members choose which categories they receive. Urgent and warning alerts are mandatory and cannot be switched off; informational ones can. Do not promise a gym that a member can be forced to receive optional categories.

Token hygiene

Tokens go stale when apps are reinstalled or uninstalled. prune-push-tokens clears out dead registrations so the platform does not report phantom devices. If a gym insists someone "has the app" but no device shows, the usual cause is that the member never granted permission or is signed in on the web only.

Testing

Settings → Push Notifications (/platform/push-notifications) sends a test to a chosen user and reports the per-device outcome — delivered, rejected, or no token. Start here before touching anything else.