skip to content

With Flutter's firebase_messaging, how do you get a device's FCM token to your backend and keep it current as it changes?

level: middleimportance: should knowfreq 48%

answer

  1. read once, then listen
  2. onTokenRefresh is not a startup event
  3. APNs token must exist first
  4. vapidKey on the web
  5. register() and onRegistered in newer source

basics

~10 s

Call FirebaseMessaging.instance.getToken() at startup or sign-in, send it to your backend, and subscribe to onTokenRefresh for later changes; on iOS the APNs token must exist first, and on web you pass a vapidKey.

solid answer

~50 s

The FCM registration token identifies this app install to FCM, so your backend needs it to target the device. Call `FirebaseMessaging.instance.getToken()` when the user signs in or the app starts, and store it against the user on your server. Then listen to `onTokenRefresh`, which emits on Android and Apple platforms when a token is generated or refreshed. It does **not** emit at every startup, and on web it is a no-op. On iOS and macOS the plugin checks for the APNs token first, and `getToken()` throws a `FirebaseException` with code `apns-token-not-set` if it has not arrived yet, so check `getAPNSToken()` and retry. On web you pass your VAPID public key as `vapidKey`. On sign-out, remove the token from your backend and call `deleteToken()`. The current flutterfire source marks these three APIs deprecated in favour of `register()`/`onRegistered`, which deliver a Firebase Installation ID.

code

dart · 38 lines
dart
import 'dart:async';

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';

abstract interface class DeviceRegistry {
  Future<void> saveToken(String userId, String token);
  Future<void> removeTokens(String userId);
}

class PushTokenSync {
  PushTokenSync(this._registry);

  final DeviceRegistry _registry;
  StreamSubscription<String>? _refresh;

  Future<void> start(String userId) async {
    final messaging = FirebaseMessaging.instance;
    final apple = defaultTargetPlatform == TargetPlatform.iOS ||
        defaultTargetPlatform == TargetPlatform.macOS;
    if (apple && await messaging.getAPNSToken() == null) {
      return; // Retry later; getToken() would throw apns-token-not-set.
    }
    final token = await messaging.getToken();
    if (token != null) await _registry.saveToken(userId, token);
    await _refresh?.cancel();
    _refresh = messaging.onTokenRefresh.listen(
      (fresh) => _registry.saveToken(userId, fresh),
    );
  }

  Future<void> stop(String userId) async {
    await _refresh?.cancel();
    _refresh = null;
    await _registry.removeTokens(userId);
    await FirebaseMessaging.instance.deleteToken();
  }
}

go deeper

for a junior

Remember the pair: getToken() once, onTokenRefresh for changes, and send both to your backend. On web the call needs a vapidKey.

for a middle

Explain why onTokenRefresh is not a startup event, why iOS throws apns-token-not-set before APNs registration finishes, and what deleteToken does on sign-out.

for a senior

Show an idempotent server upsert keyed by install, sign-out cleanup on shared devices, and how you would migrate to register() and onRegistered when your pinned version deprecates the token API.

for a principal

Frame device registration as a data-ownership problem: tokens per install versus users, cleanup of stale addresses, and what the switch from tokens to installation IDs means for backend contracts.

## What the token is for To send a push to one specific device, your server needs an address for that app installation. With **firebase_messaging**, the FlutterFire plugin for Firebase Cloud Messaging, that address is the **FCM registration token**. It belongs to the install, not to the user, and it can change. The client's job has three parts: read the token, send it to your backend, and keep the backend in step when it changes or the user signs out. ## The client API | API | Kind | What it does | |---|---|---| | `getToken({vapidKey, serviceWorkerScriptPath})` | `Future<String?>` | returns the current token | | `onTokenRefresh` | `Stream<String>` | emits when a token is generated or refreshed | | `deleteToken()` | `Future<void>` | invalidates the token; server sends to it then fail | | `getAPNSToken()` | `Future<String?>` | Apple's device token on iOS/macOS, `null` on Android and web | | `setAutoInitEnabled(bool)` | `Future<void>` | turns automatic token generation on or off, persisted across restarts | ## A reliable flow 1. **Read once.** After `Firebase.initializeApp` and, where it matters, after sign-in, call `getToken()` and send the value to your backend with the user id. 2. **Listen for changes.** Subscribe to `onTokenRefresh` and send each new value. The upstream guide stresses that this stream does **not** emit at every app startup, so step 1 is still needed on each launch. 3. **Upsert on the server.** Store tokens idempotently. Several devices per user is normal, and the same token can arrive twice. 4. **Sign-out.** Remove the token from your backend and call `deleteToken()`, so a shared device stops receiving the previous user's order updates. ## Platform traps - **iOS and macOS: the APNs token comes first.** FCM on Apple platforms rides on APNs. Before `getToken()`, `deleteToken()` and `subscribeToTopic()`, the plugin calls `getAPNSToken()`. If that is still `null`, it throws a `FirebaseException` with code `apns-token-not-set`. The APNs token is not guaranteed to exist right after permission is granted, so check `getAPNSToken()` and retry later rather than crashing startup. - **Web: `vapidKey`.** Web push needs the project's VAPID public key, passed as `getToken(vapidKey: ...)`. On web, `getToken()` also triggers the browser's permission prompt if needed, and `onTokenRefresh` never emits. - **Permission is separate on native platforms.** On Android and Apple platforms, `getToken()` does not request notification permission, so you may register before prompting. - **Auto-init.** By default a token is generated automatically. To stop that, set `FirebaseMessagingAutoInitEnabled` to `NO` in `Info.plist`, or set `firebase_messaging_auto_init_enabled` to `false` in `AndroidManifest.xml` (Android also needs Analytics collection disabled). Re-enable it at runtime with `setAutoInitEnabled(true)`. ## The newer registration API The current flutterfire source adds `register()`, `unregister()` and the `onRegistered`/`onUnregistered` streams. Their value is the **Firebase Installation ID (FID)**, which the docs describe as the direct-send target for the app instance. The source marks `getToken`, `onTokenRefresh` and `deleteToken` with `@Deprecated` pointing at them. The 16.7.0 changelog and the published guide still describe the token flow, so check which API your pinned version exposes: - On a version with `register()`: subscribe to `onRegistered`, call `register()`, and store the emitted FID. - On sign-out: call `unregister()` and remove the FID that `onUnregistered` reports. The shape of the flow is unchanged: read or register, listen for changes, upsert, and clean up. ## Where this stops How the server sends to a token, topic subscriptions, and notification versus data payloads are FCM's own semantics and belong to the Firebase messaging topic. This question is only about the Flutter client keeping your backend's copy correct.

  • Why is listening to onTokenRefresh alone not enough to keep the backend current?
    The stream emits only when a token is generated or refreshed, not on every launch. If the first emission happened before you subscribed, or the backend lost its copy, you would never resend it. Reading `getToken()` at each start or sign-in and upserting on the server closes that gap.
  • Your iOS startup crashes intermittently with apns-token-not-set. What is happening and how do you fix it?
    On Apple platforms the plugin checks `getAPNSToken()` before `getToken()` and throws when APNs registration has not finished, which can lag behind the permission grant. Check `getAPNSToken()` first and retry later, or catch that `FirebaseException` code, instead of awaiting `getToken()` unguarded during startup.

saying these in an interview costs you the question

  • onTokenRefresh emits the current token on every app launch.
  • The FCM token is tied to the user account, so store one per user.
  • getToken returns null on iOS when APNs is not ready yet.
  • The FCM token never changes once issued, so sending it once is enough.
  • Web tokens work without a VAPID key.