Users who uninstalled and reinstalled our food delivery app stopped receiving order status push notifications. Backend logs showed valid-looking tokens, but FCM returned NotRegistered for roughly 18% of reinstall users within the first session.
- Flutter 3.13, firebase_messaging 14.x, firebase_core 2.x
- Backend: Node.js storing one FCM token per userId
- Android 12–14, iOS 16–17
We only fetched the FCM token once during onboarding and never listened for onTokenRefresh. Reinstall generates a new token, but our app sent the old cached token from SharedPreferences on login because the cache key survived backup restore on some Android OEM skins.
class PushTokenService {
Future<void> initialize() async {
final messaging = FirebaseMessaging.instance;
await messaging.requestPermission();
// Always fetch fresh token on app start
final token = await messaging.getToken();
if (token != null) await _syncToken(token);
FirebaseMessaging.instance.onTokenRefresh.listen(_syncToken);
}
Future<void> _syncToken(String token) async {
final prefs = await SharedPreferences.getInstance();
final last = prefs.getString('fcm_token');
if (last == token) return;
await api.registerPushToken(token);
await prefs.setString('fcm_token', token);
}
}
Backend change: upsert tokens in a user_tokens table (userId + deviceId) instead of overwriting a single column. Mark tokens NotRegistered when FCM says so.
- Register onTokenRefresh on every app cold start
- Never trust locally cached tokens without comparing to getToken()
- Handle FCM NotRegistered in your sender worker and prune dead tokens
- QA checklist: uninstall → reinstall → verify push within 60 seconds
Push delivery rate for reinstall cohort went from 82% to 99.1%. Order-tracking notification complaints dropped by 74% in two weeks.