FCM Identifying & Targeting Users

Learn how to link Firebase Cloud Messaging registration tokens to your own user accounts

Firebase Cloud Messaging delivers to devices, not to people. To send a notification to one specific user, your backend needs to know which devices that user is signed in on. This guide covers how to link registration tokens to your user accounts, keep that link accurate through login, logout, and token rotation, and send to a single user or a segment.

How FCM targeting works

Each installation of your app receives a unique registration token from Firebase. The token identifies one app install on one device. It carries no information about who is using the app, and Firebase has no concept of a user account or a login call.

That makes the user-to-device mapping your responsibility:

  1. Your web app reads the token through the Median JavaScript Bridge.
  2. Your backend stores it against the signed-in user.
  3. When you want to reach that user, your backend looks up their tokens and sends to each one.

A user with a phone and a tablet has two tokens. A user who reinstalls the app gets a new one.

flowchart TD
    A[User signs in on Phone A] --> B[getToken returns token A]
    B --> C[Your backend stores token A for user 123]
    D[Same user signs in on Tablet B] --> E[getToken returns token B]
    E --> F[Your backend stores token B for user 123]
    C --> G[Server looks up tokens for user 123]
    F --> G
    G --> H[Send one FCM message per token]
    H --> I[Delivered to Phone A and Tablet B]

What a push provider would normally handle for you

Customer engagement platforms that sit on top of FCM and APNs hide this work behind a user model. You call a login function with your user ID, the platform's SDK collects the device token in the background, and from then on you send to "user 123" while the platform resolves that to the right devices, notices when tokens change, and discards the ones that stop working.

Firebase Cloud Messaging is the transport layer underneath those platforms, and using it directly means there is no such layer in between. You get full control of the pipeline and no per-user platform costs, and in exchange the bookkeeping moves to your backend:

ResponsibilityWith an engagement platformWith FCM directly
Collecting the device tokenDone by the platform SDKYour web app calls getToken() and posts the result to your backend
Linking a device to a userOne login callYour backend stores each token against a user ID
One user, several devicesResolved by the platformYou store one row per token and send to each
Token rotationTracked automaticallyYour app reports the token on every launch
Uninstalls and expired tokensCleaned up automaticallyYou remove tokens when Firebase rejects them
Logout and shared devicesOne logout callYou delete the row and call deleteToken()
Segments and audiencesTags and dashboard filtersTopics, or queries against your own user data
Delivery and opt-in reportingBuilt-in dashboardYour own logging

The rest of this guide walks through each of these responsibilities. None of them is complicated on its own, but all of them need to be in place before per-user targeting is reliable.


Linking a device to a user at login

Once the user has authenticated, read the device's token and post it to your backend together with your own user ID.

function linkDeviceToUser(userId) {
  median.firebaseMessaging.getToken({
    callback: function (result) {
      if (!result.token) return; // not registered yet, see below

      fetch("/api/push/devices", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "include",
        body: JSON.stringify({
          userId: userId,
          fcmToken: result.token
        })
      });
    }
  });
}

Use your own endpoint in place of /api/push/devices. On the server, take the user ID from the authenticated session rather than trusting the value in the request body, so one user can't register a device under another user's account.

If Automatic Registration is turned off in your plugin settings, no token exists until you ask for one. Request permission first, then register:

median.firebaseMessaging.requestPermission({
  callback: function (permission) {
    if (!permission.granted) return;

    median.firebaseMessaging.register({
      callback: function (result) {
        console.log("Registered, token:", result.token);
        // post result.token to your backend as shown above
      }
    });
  }
});
📘

When to link the device

Link on every app launch where the user is signed in, not only on the login page. Tokens can change without notice, and a returning user who is already authenticated never passes through your login flow.

Storing tokens on your backend

Store tokens in their own table rather than as a single column on the user record, since one user can have several devices. An example table could look as follows:

ColumnPurpose
user_idYour app's user identifier
fcm_tokenThe registration token. Make this unique across the table.
platformios or android, useful for platform-specific payloads
updated_atLast time the app reported this token, used for pruning

Make the insert an upsert keyed on fcm_token. If a token arrives that is already stored for a different user, reassign it to the new user. That is what happens when two people share a device, and it stops the first user's notifications from reaching the second.


Unlinking a device at logout

When a user signs out, remove the link in both places: delete the token from your backend, then delete it on the device.

function unlinkDeviceFromUser() {
  median.firebaseMessaging.getToken({
    callback: function (result) {
      // 1. Tell your backend to forget this token
      fetch("/api/push/devices", {
        method: "DELETE",
        headers: { "Content-Type": "application/json" },
        credentials: "include",
        body: JSON.stringify({ fcmToken: result.token })
      }).finally(function () {
        // 2. Invalidate the token on the device
        median.firebaseMessaging.deleteToken({
          callback: function (deleted) {
            console.log("Token deleted:", deleted.success);
          }
        });
      });
    }
  });
}

Read the token before you delete it, because your backend needs the value to find the row.

⚠️

Delete the token on shared devices

Removing the row from your backend is enough to stop your own targeted sends. Calling deleteToken() as well guarantees that anything still addressed to the old token is rejected by Firebase. After deleteToken(), call register() the next time a user signs in to obtain a fresh token.


Managing the token lifecycle

A registration token is stable for long periods but not permanent, and Firebase does not tell your server when one changes. Your token table drifts out of date unless the app and the backend both keep it in step. These are the events to plan for:

EventWhat happens to the tokenWhat you need to do
First launch, or first register() callA token is issuedStore it once the user is signed in
User signs inUnchangedLink the token to the user
User signs outUnchanged until you call deleteToken()Remove the row, then delete the token
A different user signs in on the same deviceUnchanged, unless it was deleted at logoutReassign the token to the new user
App reinstalled, or app data clearedA new token is issued; the old one stops workingStore the new token; prune the old one when a send fails
App restored onto a new deviceA new token is issuedStore the new token
App uninstalledThe token stops workingPrune it when a send fails
Long inactivityFirebase eventually expires the tokenPrune tokens that have not been reported recently
User turns notifications off in device settingsUnchangedNothing to prune; sends are accepted but not shown

Two patterns in this table matter most. First, your backend only learns about a new token when the app tells it, so the app has to report its token regularly. Second, your backend only learns that an old token is dead when a send to it fails, so the send path has to clean up after itself.

Report the token on every launch

Run the same link call whenever the app starts with a signed-in user. If the value is unchanged, your backend only refreshes updated_at. If it has changed, the new token is stored and the old one will be pruned the next time a send to it fails.

function median_library_ready() {
  // This example uses the function previously defined. It is not part of the standard JavaScript bridge implmentation as you have to configure it for your backend.
  if (window.currentUserId) {
    linkDeviceToUser(window.currentUserId);
  }
}

Remove tokens that Firebase rejects

When you send to a token that is no longer valid, the FCM API returns an error instead of a message ID:

{
  "error": {
    "code": 404,
    "message": "Requested entity was not found.",
    "status": "NOT_FOUND",
    "details": [
      {
        "@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError",
        "errorCode": "UNREGISTERED"
      }
    ]
  }
}

Check the response of every send and act on the error code:

Error codeHTTP statusMeaningAction
UNREGISTERED404The token is no longer valid: the app was uninstalled, the token was deleted, or it expiredDelete the token
INVALID_ARGUMENT400The token is malformed, or the payload is invalidDelete the token if the payload is known to be good
SENDER_ID_MISMATCH403The token belongs to a different Firebase projectDelete the token and check your app's Google Services files
QUOTA_EXCEEDED, UNAVAILABLE, INTERNAL429, 503, 500Temporary conditionKeep the token and retry with backoff

Expire tokens you haven't seen in a while

A device that never opens your app again will never report a new token, and you may not send to it often enough to see it fail. Firebase considers a token stale after 270 days of inactivity and will start rejecting it. Use the updated_at column to delete tokens older than a window that suits your app, and remember that reporting on every launch keeps active devices well inside it.

A token is not the same as permission

A device can hold a valid token while the user has notifications turned off. Firebase accepts the message and the device does not display it. If you need to know who can actually see your notifications, call checkPermission() when you report the token and store the result alongside it:

median.firebaseMessaging.checkPermission({
  callback: function (permission) {
    median.firebaseMessaging.getToken({
      callback: function (result) {
        // post result.token and permission.granted to your backend
      }
    });
  }
});

Sending to a specific user

Sending happens from your server through the FCM HTTP v1 API. Look up the user's tokens in your database and send one request per token.

Each request is authorized with a short-lived OAuth 2.0 access token, obtained from a service account in your Firebase project with the https://www.googleapis.com/auth/firebase.messaging scope. Access tokens last one hour, so cache and reuse them rather than requesting one per send.

curl -X POST \
  "https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "token": "DEVICE_REGISTRATION_TOKEN",
      "notification": {
        "title": "Your order has shipped",
        "body": "Tap to track your delivery."
      }
    }
  }'

A successful send returns the message ID:

{
  "name": "projects/YOUR_PROJECT_ID/messages/0:1700000000000000%abc123"
}

The HTTP v1 API accepts one target per request, so a user with three devices means three requests. Treat each response separately: one device may succeed while another returns UNREGISTERED, in which case you remove only that token as described in Managing the token lifecycle.

👍

Test a single-device send first

Before writing server code, confirm delivery with the Test Sender. Open the demo page inside your app, tap Get Token then Copy Token, and on your computer paste it into the Test Sender with Send to set to A single device (registration token). The Payload panel shows the exact JSON it sends, including fields for target URL, image, channel ID, and badge. Use Copy JSON as the starting point for your server payload.

❗️

Keep service account keys off the client

A service account key can send to every user of your Firebase project. Use it only on your server. Never embed it in your website or app, and only load a test project's key into the Test Sender.


When to use each target

TargetUse it when...
Registration tokenYou want to reach one specific user, or one specific device. Your backend owns the user-to-token mapping. This is the right choice for anything personal: order updates, direct messages, account alerts.
TopicYou want to reach everyone interested in a subject, such as breaking_news or team_updates. Firebase maintains the subscriber list and you never handle tokens.
Condition (topics combined)You want an audience built from several topics in one send, for example 'sports' in topics && 'premium' in topics.

For most apps, tokens handle personal notifications and topics handle broadcasts.

⚠️

Don't use topics for private messages

A topic such as user_123 looks like a shortcut for reaching one user across all devices without storing tokens. Topic subscriptions are requested by the client, so any app install can subscribe to any topic name. Use topics only for content you would be comfortable with any user receiving.


Segmenting with topics

Topics let you group users into segments for broadcasts: "users who follow sports," "users on the free plan," "users in the Halifax region." A device can subscribe to as many topics as you like, and subscriptions are stored per device.

Build the subscription controls in your web UI and wire them to the Median JavaScript Bridge.

Subscribe to a topic

median.firebaseMessaging.subscribeToTopic({
  topic: "sports",
  callback: function (result) {
    console.log("Subscribed:", result.success);
  }
});

Unsubscribe from a topic

median.firebaseMessaging.unsubscribeFromTopic({
  topic: "sports",
  callback: function (result) {
    console.log("Unsubscribed:", result.success);
  }
});

List subscribed topics

Use this to set the initial state of the toggles on a preferences page.

median.firebaseMessaging.getSubscribedTopics({
  callback: function (result) {
    console.log("Subscribed topics:", result.topics); // e.g. ["sports"]
  }
});

Topics that follow the user's account

Because subscriptions belong to the device, they stay behind when a user signs out and don't appear on a second device automatically. For segments that depend on account data, such as plan or region, sync them at the same points where you link and unlink the device:

// After login: subscribe to the segments this account belongs to
function syncAccountTopics(user) {
  median.firebaseMessaging.subscribeToTopic({ topic: "plan_" + user.plan });
  median.firebaseMessaging.subscribeToTopic({ topic: "region_" + user.region });
}

// Before logout: remove every subscription from this device
function clearTopics() {
  median.firebaseMessaging.getSubscribedTopics({
    callback: function (result) {
      (result.topics || []).forEach(function (topic) {
        median.firebaseMessaging.unsubscribeFromTopic({ topic: topic });
      });
    }
  });
}

Topic names may contain only letters, numbers, and the characters -_.~%.

Sending to a topic

Replace token with topic in the message. In the Test Sender, set Send to to A topic and enter the topic name.

{
  "message": {
    "topic": "sports",
    "notification": {
      "title": "Full time",
      "body": "Catch up on tonight's results."
    }
  }
}