How Did We Discover the FCM Background Delivery Issue in Flutter Web?
During a recent project for a fast-growing B2B SaaS platform, we were tasked with building a unified real-time alert system. The application required instant notifications for high-priority workflow approvals, regardless of whether the user was actively looking at the application or had minimized the browser tab. We chose Flutter for its cross-platform capabilities, allowing us to deploy seamlessly to both mobile and web.
While testing the web implementation, we noticed a critical discrepancy. When the application was the active browser tab, Firebase Cloud Messaging (FCM) worked perfectly, instantly rendering our custom in-app popups. However, the moment the tab was switched, minimized, or inactive, the application stopped receiving notifications entirely. The payload was being delivered by the backend, but the web client was completely silent.
In enterprise communication platforms, dropped alerts represent a severe operational failure. We realized that handling push notifications in a background state on a Flutter Progressive Web App (PWA) introduces complex service worker lifecycle challenges. This issue is incredibly common, and we are sharing our diagnostic and resolution process so other engineering teams can avoid the same architectural pitfalls.
Why Did the Web Architecture Struggle With Inactive Tabs?
To understand the problem, we must look at how FCM interacts with web browsers. When a web application is in the foreground, the main JavaScript thread (or Dart thread, in Flutter’s case) processes incoming messages directly. When the application moves to the background, the browser pauses the main thread to conserve memory and battery. Any incoming messages are then handed over to a background Service Worker.
In our initial implementation, the codebase relied on a standard firebase-messaging-sw.js file using the compat SDK. The business logic dictated that when a background message arrived, the service worker should intercept it and trigger a native browser notification. Clicking the notification should then focus the inactive tab or open a new one with a specific routing payload. Yet, none of this was happening. It was as if the service worker did not exist or had gone completely dormant.
What Caused the Silent Failure in the Service Worker Lifecycle?
By diving into the Chrome DevTools under the Application tab, we began inspecting the active service workers. We identified two primary architectural oversights that led to the background message failure.
First, Flutter web applications automatically generate and register their own service worker (often named flutter_service_worker.js) to manage caching and PWA asset delivery. At the same time, Firebase expects to operate within a specific file named firebase-messaging-sw.js by default. When multiple service workers attempt to control the same scope without explicit delegation, browsers will often discard the messaging worker in favor of the framework’s primary worker.
Second, our initial service worker code contained conflicting client claims. We used self.skipWaiting() and clients.claim() aggressively. While this is standard practice for immediate updates, it inadvertently disrupted Flutter’s own application initialization loop. As a result, the FCM service worker was either failing to register properly during the Flutter startup sequence, or the background push events were not successfully reaching the Firebase SDK because the service worker scope was overridden.
This is a common hurdle when companies decide to hire flutter developers for web applications—the nuances of web-specific threading often conflict with mobile-first Dart patterns.
What Solutions Did We Evaluate for Background Messaging?
Resolving service worker conflicts requires a careful balancing act between the UI framework and the push notification provider. Before settling on our final implementation, we evaluated several different architectural approaches.
Should We Combine the Flutter and Firebase Service Workers?
Our first thought was to merge the Firebase messaging logic directly into the generated flutter_service_worker.js. While this guarantees only one service worker controls the scope, it is highly unmaintainable. Every time the Flutter application is built, the Flutter CLI rewrites the service worker file. Modifying build scripts to append JavaScript after every compilation is fragile and breaks easily across CI/CD pipelines.
Can We Explicitly Bind the Service Worker Registration in Dart?
We explored registering the firebase-messaging-sw.js manually in the root index.html file and then passing that specific registration object to the Dart Firebase SDK. This approach ensures that Firebase uses our custom worker for routing notifications while letting Flutter’s default worker handle the PWA caching. This seemed like the cleanest architectural boundary.
Could We Fall Back to Legacy Web Socket Connections?
We briefly considered ignoring FCM for background web states and keeping an active Web Socket connection alive. However, modern browsers severely restrict background connections for battery optimization. A Web Socket would eventually be throttled or disconnected by the browser, making it unreliable for an enterprise notification system. This is why many organizations prefer to hire firebase experts for real-time systems to properly configure native browser push APIs.
How Did We Finally Implement the FCM Service Worker Fix?
We ultimately chose the explicit registration path. We created a sanitized, standalone Service Worker specifically for Firebase, completely decoupled from Flutter’s asset-caching worker. We then updated the web entry point to explicitly pass this worker to Firebase before the Dart application initialized.
Below is the refined and sanitized implementation of our firebase-messaging-sw.js file. We ensured it safely handles the payload and gracefully opens or focuses the correct application tab.
importScripts('https://www.gstatic.com/firebasejs/10.7.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging-compat.js');
firebase.initializeApp({
apiKey: "YOUR_API_KEY",
appId: "YOUR_APP_ID",
messagingSenderId: "YOUR_SENDER_ID",
projectId: "YOUR_PROJECT_ID",
authDomain: "YOUR_AUTH_DOMAIN",
storageBucket: "YOUR_STORAGE_BUCKET",
measurementId: "YOUR_MEASUREMENT_ID",
});
const messaging = firebase.messaging();
self.addEventListener('install', (event) => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});
messaging.onBackgroundMessage((payload) => {
const title = payload.notification?.title || "System Alert";
const body = payload.notification?.body || "You have a new message.";
const route = payload.data?.route || "/";
self.registration.showNotification(title, {
body: body,
icon: "/icons/Icon-192.png",
data: { route: route }
});
});
self.addEventListener('notificationclick', function(event) {
event.notification.close();
const route = event.notification.data?.route || "/";
event.waitUntil(
clients.matchAll({ type: "window", includeUncontrolled: true })
.then((clientList) => {
for (const client of clientList) {
if (client.url.includes(self.location.origin) && "focus" in client) {
client.focus();
client.navigate("/#" + route);
return;
}
}
return clients.openWindow("/#" + route);
})
);
});
To ensure this service worker did not conflict with Flutter, we adjusted the index.html file to register the messaging worker independently and safely handle the promise. In the Dart code, we utilized the FirebaseMessaging.instance.getToken() method, explicitly passing the VAPID key to ensure backend synchronization was flawless.
What Should Technical Teams Learn From This Implementation?
Architecting multi-platform notifications requires a deep understanding of platform-specific web limitations. Here are the key takeaways for teams dealing with background push notifications:
- Isolate Responsibilities: Never attempt to merge external service worker logic into auto-generated framework files. Keep PWA asset caching and push messaging strictly separated.
- Understand Browser Throttling: Do not rely on Dart background loops for web push. Modern browsers will aggressively suspend the JavaScript thread. Background tasks must live natively within the Service Worker.
- Handle Missing Payloads Gracefully: Always provide fallback values for notification titles and bodies. Malformed payloads can cause the service worker to crash silently, dropping the alert.
- Optimize the Notification Click: Looping through clients.matchAll ensures you focus an existing application tab rather than opening a duplicate window, dramatically improving the user experience.
- Test Across Multiple Browsers: Safari, Chrome, and Firefox handle service worker activation slightly differently. Always perform cross-browser testing for lifecycle events.
- Secure Your Configurations: Ensure your Service Worker does not expose sensitive business logic. Use standard environment variables during your build process to inject Firebase config keys safely.
How Can You Apply This to Your Future Application Builds?
Web applications have matured into highly complex desktop-like experiences, but unlocking that native-like performance requires deep architectural insight. Properly configuring service workers ensures your platform remains reliable, keeping users engaged even when they are focused on other tasks. By explicitly delegating background tasks and avoiding framework auto-generation conflicts, we stabilized the notification delivery pipeline and ensured 100% alert reliability.
If your organization is scaling complex cross-platform architecture and you are looking to hire software developer experts to stabilize your infrastructure, our pre-vetted remote engineering teams can help. Whenever you need to hire dedicated flutter developers who understand the intricacies of enterprise web deployments, contact us to discuss your project requirements.
Social Hashtags
#Flutter #FlutterWeb #Firebase #FCM #PushNotifications #WebDevelopment #ServiceWorker #PWA #FirebaseMessaging #FlutterDevelopment #MobileAppDevelopment #SoftwareDevelopment
Frequently Asked Questions
Mobile platforms use native operating system-level daemons to listen for push notifications. Flutter Web relies on the browser's Service Worker API. If the service worker is missing, unregistered, or suspended improperly, the background push events will not execute.
No, while our snippet demonstrates the compat implementation due to legacy constraints, it is highly recommended to migrate to the Firebase v10+ modular SDK. The modular SDK reduces the bundle size significantly, though the lifecycle management principles remain exactly the same.
This happens when the clients.openWindow() method is passed a poorly formatted URL or route. Ensure that your payload data correctly matches your Flutter application's routing strategy (e.g., hash routing vs. path routing) and that the origin matches.
Yes. In Chrome, open Developer Tools, navigate to the Application tab, and select Service Workers. You can manually trigger a push event, force the worker to update, or inspect any console errors that occur entirely independent of the Flutter UI thread.
Success Stories That Inspire
See how our team takes complex business challenges and turns them into powerful, scalable digital solutions. From custom software and web applications to automation, integrations, and cloud-ready systems, each project reflects our commitment to innovation, performance, and long-term value.

California-based SMB Hired Dedicated Developers to Build a Photography SaaS Platform

Swedish Agency Built a Laravel-Based Staffing System by Hiring a Dedicated Remote Team

















