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:
- Your web app reads the token through the Median JavaScript Bridge.
- Your backend stores it against the signed-in user.
- 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:
| Responsibility | With an engagement platform | With FCM directly |
|---|---|---|
| Collecting the device token | Done by the platform SDK | Your web app calls getToken() and posts the result to your backend |
| Linking a device to a user | One login call | Your backend stores each token against a user ID |
| One user, several devices | Resolved by the platform | You store one row per token and send to each |
| Token rotation | Tracked automatically | Your app reports the token on every launch |
| Uninstalls and expired tokens | Cleaned up automatically | You remove tokens when Firebase rejects them |
| Logout and shared devices | One logout call | You delete the row and call deleteToken() |
| Segments and audiences | Tags and dashboard filters | Topics, or queries against your own user data |
| Delivery and opt-in reporting | Built-in dashboard | Your 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 deviceLink 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:
| Column | Purpose |
|---|---|
user_id | Your app's user identifier |
fcm_token | The registration token. Make this unique across the table. |
platform | ios or android, useful for platform-specific payloads |
updated_at | Last 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 devicesRemoving 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. AfterdeleteToken(), callregister()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:
| Event | What happens to the token | What you need to do |
|---|---|---|
First launch, or first register() call | A token is issued | Store it once the user is signed in |
| User signs in | Unchanged | Link the token to the user |
| User signs out | Unchanged until you call deleteToken() | Remove the row, then delete the token |
| A different user signs in on the same device | Unchanged, unless it was deleted at logout | Reassign the token to the new user |
| App reinstalled, or app data cleared | A new token is issued; the old one stops working | Store the new token; prune the old one when a send fails |
| App restored onto a new device | A new token is issued | Store the new token |
| App uninstalled | The token stops working | Prune it when a send fails |
| Long inactivity | Firebase eventually expires the token | Prune tokens that have not been reported recently |
| User turns notifications off in device settings | Unchanged | Nothing 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 code | HTTP status | Meaning | Action |
|---|---|---|---|
UNREGISTERED | 404 | The token is no longer valid: the app was uninstalled, the token was deleted, or it expired | Delete the token |
INVALID_ARGUMENT | 400 | The token is malformed, or the payload is invalid | Delete the token if the payload is known to be good |
SENDER_ID_MISMATCH | 403 | The token belongs to a different Firebase project | Delete the token and check your app's Google Services files |
QUOTA_EXCEEDED, UNAVAILABLE, INTERNAL | 429, 503, 500 | Temporary condition | Keep 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 firstBefore 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 clientA 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
| Target | Use it when... |
|---|---|
| Registration token | You 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. |
| Topic | You 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 messagesA topic such as
user_123looks 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."
}
}
}Updated about 1 hour ago