← All articles
Integrations10 min read

Push Notifications in Flutter with Firebase: a complete setup guide

End-to-end FCM setup for Android and iOS — APNs keys, permissions, foreground and background handling, deep links, topics and tokens, and why notifications silently fail in production.

FlutterFirebaseFCMNotifications

Push notifications are simple in principle and full of small traps in practice. The common experience is that everything works in debug on Android and silently fails on iOS in production. This guide walks through a complete Firebase Cloud Messaging setup and the specific places delivery breaks.

How delivery actually works

Your server never talks to the device. It sends a message to FCM, which routes it to Google services on Android or to Apple Push Notification service on iOS, which delivers it to the device. Two consequences follow:

  • You cannot guarantee delivery. Push is best-effort. Anything critical also needs to be fetched when the app opens.
  • On iOS, FCM needs credentials to talk to APNs. Missing that step is the single most common cause of “works on Android, nothing on iOS”.

Setup, in order

1. Firebase project and app registration

Create the Firebase project, register the Android app with its package name, register the iOS app with its bundle identifier, and add google-services.json and GoogleService-Info.plist to the right folders. The identifiers must match the app exactly, including for flavours — a staging build with a different package name is a different app and needs its own registration.

2. The iOS half that people miss

Three things are required and each is separately easy to forget:

  • An APNs authentication key created in the Apple Developer account and uploaded to Firebase. A single key works for all your apps and does not expire, which makes it preferable to certificates.
  • Push Notifications capability enabled on the App ID and in Xcode.
  • Background Modes → Remote notifications enabled if you want notifications to wake the app.

Also note that push does not work on the iOS simulator in the way it does on device. Test on real hardware.

3. Permission, asked at the right moment

iOS requires explicit permission; Android has since Android 13. A permission prompt on first launch, before the user knows what the app does, gets declined and cannot easily be asked again.

Ask when the value is obvious — after the user places an order, follows a seller, or completes setup — with one line explaining what they will receive.

Notification vs data messages

This distinction explains most confusing behaviour.

TypeApp in foregroundApp in background or closed
Notification (has a notification block)Delivered to your handler; the system does not display itThe system displays it automatically; your code does not run until tapped
Data-only (only a data block)Delivered to your handlerWakes your background handler; you must display it yourself

The practical consequence: with a notification message, a foreground push appears to do nothing, because the system deliberately suppresses it. You have to display it yourself using a local-notifications package. That is expected behaviour, not a bug.

The three handlers you need

// 1. Background / terminated — must be a top-level function
@pragma('vm:entry-point')
Future<void> _bgHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  // keep this short: show a local notification, update a badge
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  FirebaseMessaging.onBackgroundMessage(_bgHandler);
  runApp(const MyApp());
}

// 2. Foreground
FirebaseMessaging.onMessage.listen((m) => showLocalNotification(m));

// 3. Tapped while app was in the background
FirebaseMessaging.onMessageOpenedApp.listen(handleDeepLink);

// ...and the cold-start case, easily forgotten:
final initial = await FirebaseMessaging.instance.getInitialMessage();
if (initial != null) handleDeepLink(initial);

That last one matters. If the app was fully closed when the notification was tapped, onMessageOpenedApp does not fire. Without getInitialMessage(), tapping a notification opens the app on the home screen instead of the intended content.

Deep linking to the right screen

Send an identifier in the data payload rather than a full route string, and resolve it in the app:

{ "type": "order", "id": "10432" }

Then decide what happens if the user is not logged in, or no longer has access to that order. Navigating blindly to a screen that then errors is worse than opening the home screen with a message.

Tokens: the part that breaks quietly

Each install has a registration token, and tokens change. They rotate, they change on reinstall, and they are invalidated when app data is cleared.

  • Send the token to your server on login and listen to onTokenRefresh.
  • Delete the token on logout, or the next user of that device receives the previous user's notifications.
  • Remove tokens your server is told are invalid. Sending to dead tokens forever is a slow leak.
  • Store one row per token, not one per user. People have several devices.

For broadcasts, topics are simpler than managing lists of tokens: subscribe devices to news or offers and publish once.

Android specifics

  • Channels are mandatory on modern Android. Create them at startup and use meaningful names, because users can disable individual channels.
  • Aggressive battery optimisation on some manufacturers delays or drops background delivery. If your users report missing notifications on particular brands, this is usually why, and it is a device-level setting rather than a bug in your app.
  • Set a notification icon and colour. Otherwise you get a grey square.

Testing checklist

  • Foreground, background and fully closed, on both platforms.
  • Tapped from a cold start — does it reach the right screen?
  • Permission denied, then re-enabled in settings.
  • Logged out — does the device stop receiving the previous user's pushes?
  • Reinstalled app — is the new token registered?
  • A release build on a real device, not just debug.
  • Airplane mode on, message sent, airplane mode off.

Content rules worth remembering

Notification text can appear on a lock screen in public. Do not put sensitive details in it — medical results, transaction amounts, message contents for private chats — unless that has been a deliberate decision. Send an identifier and let the app fetch the detail after the user unlocks and taps.

Read next