FCM Token Hygiene and Push Reliability for Solo Flutter + Firebase Founders in 2026
Push notifications are often the only re-engagement channel a solo Flutter app has, and they decay quietly. Users change phones, uninstall, deny the permission prompt, or simply stop opening the app. Your server keeps sending to device registrations that will never deliver, your delivery numbers drop for no clear reason, and a retry loop that hammers FCM on errors can make things worse. This 2026 guide is a practical token-hygiene and reliability setup for indie Flutter + Firebase Cloud Messaging (FCM) apps: how to store registrations with timestamps, prune stale and invalid ones, handle permissions on Android and iOS, retry safely, and review delivery once a month. It pairs with the Remote Config kill-switch guide, Cloud Functions cost alerts and budgets, and the Firestore Security Rules review cadence. Console labels and SDK APIs change; confirm details against the official Firebase docs linked below. Portfolio: gspteck.com.
Why token hygiene matters more than the send code
Sending a push from Cloud Functions with the Admin SDK takes a few lines. Keeping it reliable for a year takes a data model. Firebase’s best practices for FCM registration management define a stale registration as one whose device has not connected to FCM for over a month, and note that on Android a registration inactive for 270 days is treated as expired and rejected. Sends to stale registrations are unlikely ever to be delivered, and they make delivery reports look worse than reality.
The same page notes that FCM is moving from registration tokens toward registrations based on Firebase Installation IDs (FIDs), with both patterns supported and the same best practices applying to both. In Flutter today, the firebase_messaging plugin gives you a token through getToken() and a stream of updates through onTokenRefresh, as shown in the Flutter client setup guide. Whatever identifier you store, the rules below stay the same.
- One user, many devices. Store one record per app installation, not one token per user.
- Every record has a timestamp updated whenever the app uploads its registration.
- Invalid means delete. Registrations FCM rejects as unregistered should leave your database right away.
- Stale means stop sending. Pick a staleness window (a month is the documented default) and prune past it.
A minimal Firestore data model
For a solo app, a subcollection per user works well: users/{uid}/devices/{installationKey}, with fields such as token, platform, appVersion, locale, and updatedAt (a server timestamp). Write it from the app when the user is signed in:
- On app start (after sign-in), read the current token and write it with a fresh server timestamp. The Firebase guide recommends updating the timestamp on every upload, even if the token didn’t change, because that timestamp is your signal that the installation is alive.
- On
onTokenRefresh, write the new value to the same device document. The Flutter docs note this stream also fires at app startup, so the write should be idempotent. - On sign-out, delete the device document (and optionally the token) so a shared phone doesn’t keep receiving the previous user’s notifications.
- Lock it down. Only the signed-in user should write their own device documents; your Cloud Functions read them with the Admin SDK. Treat this like any other rules path in your review.
Keep personal data out of the device record beyond what you need to target messages, and make sure your account-deletion flow removes these documents too. The Play Console privacy and data safety guide covers why that matters for store listings.
Permissions: Android 13+ and iOS
A valid token does not mean the user will see your notification. On Android 13 and higher, apps must request the POST_NOTIFICATIONS runtime permission, as described in Android’s notification runtime permission guide. On Apple platforms, apps must ask permission to use notifications, and FCM needs an APNs authentication key uploaded in the Firebase console (see configuring APNs with FCM).
- Ask in context. Request permission when the user turns on a feature that needs it (for example “remind me”), not on the first screen.
- Record the permission state with the device record so you can tell “token exists but notifications are off” from a delivery failure.
- On iOS, wait for the APNs token. The Flutter guide recommends confirming
getAPNSToken()returns a value before making other FCM plugin calls on Apple platforms. - Watch for auth errors.
THIRD_PARTY_AUTH_ERRORin the FCM error reference means the APNs certificate or web push key is invalid or missing, which affects every iOS user at once.
Handle send errors without making them worse
The FCM HTTP v1 error code reference is the most useful page to keep open while writing your send function. Group errors into three buckets:
- Delete the registration:
UNREGISTERED(404) means the app instance is no longer registered.INVALID_ARGUMENT(400) means the same only if you are sure the payload is valid, since it also covers payload mistakes. - Fix your configuration:
SENDER_ID_MISMATCH(403) usually means you are sending from the wrong Firebase project, a common mix-up when staging and production apps share code.THIRD_PARTY_AUTH_ERROR(401) points to APNs or web push credentials. - Retry with backoff:
UNAVAILABLE(503) andINTERNAL(500) should be retried with exponential backoff, honoringRetry-Afterwhen present.QUOTA_EXCEEDED(429) means slow down.
Firebase’s scaling guidance recommends exponential backoff with jitter and warns that retrying without them causes “retry amplification.” For a solo app this mostly means: never retry in a tight loop inside a function, cap retries, and spread large sends over time instead of firing all of them at the same minute. Unbounded retries are also how a push bug turns into a surprise bill, so keep the alerts from the cost alerts guide in place.
Prune stale registrations on a schedule
Deleting invalid tokens only catches registrations FCM has already given up on. A daily or weekly scheduled function closes the rest of the gap:
- Query device documents whose
updatedAtis older than your staleness window (30 days is the default in the Firebase guide; some apps use 60). - Unsubscribe them from topics with the Admin SDK if you use topic messaging. The guide suggests having the app resubscribe whenever its registration changes, so returning users recover automatically.
- Delete or archive the stale documents.
- Log counts (checked, pruned, errors) so you can see the trend over time rather than individual tokens.
A returning user doesn’t lose anything: the next app start writes a fresh registration and timestamp.
A monthly 20-minute push review
- Delivery data. Check the Messaging reports in the Firebase console and, for Android, the delivery data described in understanding message delivery. A high share of drops due to inactive devices means pruning is overdue.
- Prune job health. Did it run every scheduled time? Are pruned counts reasonable?
- Error mix. Spikes in
UNREGISTEREDafter a release can point to an install or sign-out bug; anyTHIRD_PARTY_AUTH_ERRORneeds a same-day fix. - Permission rate. If most new users decline, move the prompt later in the flow.
- Message value. Fewer, more useful notifications keep users from disabling them entirely. Use Remote Config to switch off a campaign that misbehaves.
Checklist for indie FCM token hygiene
- One device document per installation with a server timestamp, updated on every app start and token refresh.
- Device documents deleted on sign-out and account deletion; rules restrict writes to the owner.
- Permission requested in context on Android 13+ and iOS; APNs key uploaded and checked.
UNREGISTEREDregistrations deleted immediately; configuration errors alerted.- Retries capped with exponential backoff and jitter; large sends spread out.
- Scheduled pruning of stale registrations and topic unsubscribes; monthly review on the calendar.
Key takeaways
- Push reliability is mostly a data-model problem: timestamps, per-device records, and pruning.
- Stale registrations waste sends and distort delivery metrics; remove them on a schedule.
- Treat FCM error codes as instructions: delete, fix configuration, or back off.
- Permissions and APNs credentials fail silently unless you record and check them.
- A monthly 20-minute review is enough for most solo Flutter + Firebase apps.
Sources
- Firebase: Best practices for FCM registration management
- Firebase: Set up an FCM client app on Flutter
- Firebase: FCM HTTP v1 ErrorCode reference
- Firebase: Best practices when sending FCM messages at scale
- Firebase: Understanding message delivery
- Android Developers: Notification runtime permission
- Apple Developer: Asking permission to use notifications
Operational guidance for indie developers, not a guarantee of delivery. FCM registration behavior, quotas, and APIs are changing (including the move toward Firebase Installation ID-based registrations); confirm current behavior in the official Firebase, Android, and Apple documentation for your apps. Last verified 2026-10-06.