Order-status pushes in a React Native pharmacy app silently stop reaching some users after weeks; how do you keep device tokens current on the backend?
answer
- tokens rotate without asking
- refresh listener plus re-send on launch
- one row per installation, upsert
- sign-out detaches, sign-in attaches
- prune on unregistered errors
basics
~20 sTokens rotate, so the app must re-send them: fetch the token on each launch and sign-in, subscribe to onTokenRefresh or addPushTokenListener, upsert one row per installation, detach on sign-out, and delete tokens the push service reports unregistered.
solid answer
~40 sSilent loss usually means the backend still holds a **stale token**. Fix both ends. In the app: fetch the current token on every launch and after sign-in, and subscribe to changes, `onTokenRefresh(getMessaging(), cb)` with React Native Firebase or `Notifications.addPushTokenListener(cb)` with `expo-notifications`, registered at app start rather than on one screen. Send it with an **installation ID** so the backend can **upsert** one row per install instead of appending duplicates, and retry uploads that fail offline. On sign-out, detach the token from the user (or delete it) so the next person on the device does not receive someone else's prescriptions. On the backend, delete or disable tokens when the sender reports them unregistered, such as Expo's `DeviceNotRegistered` receipt, and keep separate rows for each of a user's devices.
code
tsx · 27 linesimport { useEffect } from 'react';
import { Platform } from 'react-native';
import { getMessaging, getToken, onTokenRefresh } from '@react-native-firebase/messaging';
async function upsertDevice(installationId: string, token: string) {
const res = await fetch('https://api.example.com/v1/devices/' + installationId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, provider: 'fcm', platform: Platform.OS }),
});
if (!res.ok) throw new Error('device upsert failed: ' + res.status);
}
export function usePushTokenSync(userId: string | null, installationId: string) {
useEffect(() => {
if (!userId) return;
const messaging = getMessaging();
getToken(messaging)
.then(token => upsertDevice(installationId, token))
.catch(() => {
// keep going: the next launch or sign-in retries the upsert
});
return onTokenRefresh(messaging, token => {
upsertDevice(installationId, token).catch(() => {});
});
}, [userId, installationId]);
}go deeper
Recall that push tokens can change, so the app sends the current one to the backend again rather than only once.
Explain onTokenRefresh and addPushTokenListener, where to register them, and why the backend upserts per installation.
Diagnose silent push loss from stale tokens, and design sign-out detachment, retrying uploads and pruning on unregistered errors.
Own the device-registration contract between app and backend, including privacy rules for shared devices and metrics that reveal token churn.
## Why tokens go stale A device token identifies one installation to APNs or FCM, and it can be replaced without the user doing anything visible: - the provider rotates it; - the user reinstalls the app, clears its data, or restores onto a new phone; - the app moves between APNs environments (a development build and a TestFlight or store build on the same device register different tokens). If the app only sent its token once, at first sign-in, the backend keeps sending order updates to an address that no longer reaches anyone. Nothing crashes; pushes just stop. That matches the symptom of users who "stopped getting notifications after a few weeks". ## The app side **1. Re-send on every launch and sign-in.** Fetching the token is cheap because libraries cache it, and re-sending an unchanged token is harmless if the backend upserts. This also self-heals any earlier failed upload. **2. Listen for rotation at app start.** | Library | Get current token | Listen for changes | |---|---|---| | React Native Firebase | `getToken(getMessaging())` | `onTokenRefresh(getMessaging(), token => ...)`, returns an unsubscribe function | | expo-notifications (native token) | `Notifications.getDevicePushTokenAsync()` | `Notifications.addPushTokenListener(token => ...)`, returns a subscription with `remove()` | | expo-notifications (Expo token) | `Notifications.getExpoPushTokenAsync({ projectId })` | the library re-registers rotated native tokens with Expo itself; you still re-send the Expo token to your backend on sign-in | Register listeners in a component or module that lives for the whole app session, not in a settings screen that may never mount. **3. Identify the installation.** Send an installation ID the app generates once and stores locally, alongside the token, platform and provider type (`fcm`, `apns` or `expo`). The backend can then replace the old token for that installation instead of accumulating dead rows. **4. Retry uploads.** The token often changes while the device is offline or the user is signed out. Keep the latest unsent token and retry on the next launch or sign-in. **5. Handle sign-out.** Detach the token from the account on the backend, or delete it with `deleteToken(getMessaging())` in React Native Firebase. A shared family tablet must not keep receiving the previous user's prescription updates. ## The backend side - **One row per installation**, keyed by installation ID, with `user_id`, `token`, `provider`, `platform`, `updated_at`. - **Upsert** on every report; never append blindly. - **Fan out to all rows** for a user, since users have several devices. - **Prune** rows when the sending service reports the token is gone. With Expo's push service that is a push receipt whose error is `DeviceNotRegistered`; direct FCM and APNs integrations return their own equivalent errors. - **Watch the rate** of send failures per platform; a sudden jump often means a credential or environment problem rather than normal churn. ## Diagnosing the specific report 1. Look up the affected user's rows: is `updated_at` old, and does a second, newer installation exist? 2. Check send results for their tokens: unregistered errors mean the app failed to report a new token. 3. Reproduce on a device: log the token on launch, force a change (reinstall), and confirm the backend row updates. 4. Check whether the listener is registered on a code path that runs for every user, including users who skip onboarding. ## Testing the refresh path Rotation is rare in day-to-day development, so test it on purpose: - reinstall the app on a device and confirm the backend row for that installation now carries the new token; - with React Native Firebase, call `deleteToken(getMessaging())` to invalidate the FCM token, run the app's registration step again, and check that the backend row now holds the new token; - sign out and back in as a different user on the same device and confirm the row moved to the new account. ## Why this matters for this domain Order-status pushes are often the main reason users keep a pharmacy app installed, and they may carry health-related information. Sending to the wrong account after a sign-out is worse than not sending, so the sign-out step is not optional polish; it is part of keeping the token table correct.
- Why key the backend's token table by installation rather than by token string?When a token rotates, the app reports a new string. Keyed by token, the backend inserts a new row and keeps the dead one, so the user gets duplicates until the old one errors, and the table grows forever. Keyed by an installation ID the app stores once, the backend replaces the token in place, and a user's devices remain one row each.
- What should happen to the device token when a user signs out of the pharmacy app?Detach it from that account on the backend before or as part of sign-out, or delete it on the device with React Native Firebase's `deleteToken`. Otherwise the next person to sign in on that device, or nobody at all, keeps receiving the previous user's order updates, which may contain health information.
saying these in an interview costs you the question
- Sending the token once at first sign-in is enough
- Tokens only change when the user reinstalls the app
- The refresh listener can live on the notifications settings screen
- Store one token per user and overwrite it on each report
- A token for a signed-out user can stay attached to the account