Gemini said
I’ve reviewed your Service Worker code. It’s well-structured, but there is a critical architectural "race condition" occurring here that is likely causing the "hit or miss" behavior on mobile.

On a desktop, the browser keeps the Service Worker "warm" or gives it plenty of time to boot. On mobile (especially Android), the OS is much more aggressive.

1. The onBackgroundMessage vs. event.waitUntil Problem
In your Firebase Messaging block:

JavaScript
messaging.onBackgroundMessage((payload) => {
  // ... logic ...
  return showRosterChirpNotification({...});
});
The Flaw: The Firebase onBackgroundMessage handler is a callback, but it is not being awaited by the Service Worker's underlying push event.

When a push arrives, the browser fires a native push event. The Firebase SDK intercepts this and triggers your onBackgroundMessage. However, because you aren't explicitly telling the browser to "stay awake" while Firebase does its thing, the mobile OS often kills the Service Worker thread before the Firebase SDK even finishes parsing the message or before showRosterChirpNotification completes.

2. The "Double Listener" Conflict
You have both messaging.onBackgroundMessage and a manual self.addEventListener('push').

You added a check if (messaging) return; inside the push listener to prevent double-firing.

The Catch: In 2026, the firebase-messaging-compat SDK actually needs that native push event to stay alive. By returning early, you might be accidentally telling the browser "I'm done here," allowing it to terminate the process before the Firebase logic finishes.

The Fix: "Promisifying" the Background Handler
To fix the reliability on mobile, you need to ensure the native push event wraps the Firebase logic in a Promise.

Modify your code to look like this:

JavaScript
// 1. Remove the return showRosterChirpNotification from onBackgroundMessage
// 2. Wrap the logic in the native 'push' event to guarantee execution.

self.addEventListener('push', (event) => {
  console.log('[SW] Push received');

  // We use event.waitUntil to tell the Mobile OS: "Don't kill me yet!"
  event.waitUntil(
    // If Firebase is ready, we let it process, but we must make sure 
    // we return a promise that resolves only AFTER the notification shows.
    Promise.resolve().then(async () => {
      try {
        // You can still use Firebase logic, but ensure it's awaited.
        // Or, for maximum mobile reliability, parse the event.data directly:
        const payload = event.data ? event.data.json() : null;
        
        if (payload) {
          // Manually handle the notification display here to ensure 
          // the 'waitUntil' scope covers it.
          return showRosterChirpNotification({
            title: payload.notification?.title || payload.data?.title || 'New Message',
            body: payload.notification?.body || payload.data?.body || '',
            url: payload.data?.url || '/',
            groupId: payload.data?.groupId || ''
          });
        }
      } catch (err) {
        console.error('[SW] Push processing failed', err);
      }
    })
  );
});
3. Why Desktop works but Mobile fails
Desktop: The browser process is persistent. It waits for the Firebase callback to finish even if you don't use event.waitUntil.

Mobile: The moment the push event function finishes executing (which happens instantly if it's just a bunch of if statements and callbacks), the OS is allowed to freeze the thread.

4. HAProxy / PFSense Check (The "Vary" Header)
Since you are using importScripts for Firebase:
If HAProxy is caching your sw.js but serving it with a content-type or encoding that makes the mobile browser hesitate, it will fail to update. Ensure HAProxy isn't stripping the Service-Worker-Allowed header if you've set one, and make sure your server sends Cache-Control: no-cache for the sw.js file specifically.

Summary of adjustments:
Remove the if (messaging) return; from your push listener.

Consolidate your logic inside the push listener using event.waitUntil().

Check Firebase Console: Ensure the messages you send from your server are marked as High Priority (priority: 'high'), otherwise mobile devices will "batch" them and they'll arrive 10-20 minutes late.