Push: one device token per person per app — a second same-platform device silently replaces the first
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 39.9k
- Forks
- 3.4k
- Avg merge
- 6h 51m
- Merged PRs (30d)
- 232
Description
Summary
A person can hold only one push token per app. A second device registering on the same platform silently replaces the first, so only the most recently registered device remains reachable. Registration returns 200, the property is written, and the previously-registered device simply stops receiving — no error, no metric, and no way to enumerate what was lost.
Cross-platform is unaffected (Android + iOS are different app_ids). The collision is between devices sharing an app_id.
Why this happens
The subscription endpoint keys the person property on the app, not the device — push_subscriptions.py:
property_key = f"$device_push_subscription_{app_id}"
properties = {"$set": {property_key: _encrypted_fields.encrypt(device_token)}}
app_id is the Firebase project_id or the APNs bundle_id. There is nowhere in that key to put a device identifier, and this is the only write path in the file — no read-modify-write, no list handling.
$set then overwrites unconditionally — person-update.ts#L383:
updatedPerson.properties[key] = value
And the read side assumes a scalar — push-subscription-utils.ts returns a single decrypted string, which buildFcmMessage sends to as token.
Reproduction
Two Android devices for one signed-in user, same Firebase project:
POST /api/push_subscriptions/ {distinct_id: "u1", device_token: "TOKEN-PHONE", platform: "android", app_id: "my-project"}
POST /api/push_subscriptions/ {distinct_id: "u1", device_token: "TOKEN-TABLET", platform: "android", app_id: "my-project"}
Both return 200. Both resolve to $device_push_subscription_my-project. The person ends up with one property whose decrypted value is TOKEN-TABLET. The phone is no longer addressable.
Adding an iOS device produces a second property ($device_push_subscription_<bundle-id>), confirming the key is per-app rather than per-device.
What works and what doesn't
| Android phone + iPhone | ✅ different app_id → two properties, both reachable |
| Android phone + Android tablet / Android TV | ❌ same app_id → one property, last writer wins |
This only surfaces after identify(). While devices are anonymous they hold separate distinct_ids and are separate persons; once the user signs in on both, they collapse onto one person and collide.
Impact
Measured against our own production device registry (a streaming service, India, ~12M device records, active devices only):
- 17.8% of users with an Android device have two or more of them.
- Including Apple, roughly 1 in 5 signed-in users owns multiple devices on a single platform.
Among those multi-device Android users, the patterns are:
| pattern | share of multi-device Android users |
|---|---|
| Two or more Android phones | 55% |
| Android phone + Android TV | 42% |
| Two or more Android TVs | 27% |
(Patterns overlap, so these do not sum to 100%.)
The largest group is the plainest one: users with more than one Android phone. An upgrade where the old handset is still signed in, a work and a personal phone, a shared household account — all common, none exotic, and all reduced to a single reachable device. This is not an edge case introduced by unusual hardware; it is the ordinary consumer situation.
The pattern with the worst failure mode is a TV variant. Our Android mobile and Android TV apps ship in the same Firebase project, which is normal — one project, multiple package names. The TV and the phone therefore share an app_id, and the TV registering steals the token from the phone. Since a TV is frequently the most recently opened device, notifications end up targeted at a switched-off television while the user's phone receives nothing.
Both cases share the same root: the property key has room for an app, but not for a device. Any second device on the platform wins, whatever it is.
Beyond delivery, the property cannot answer "which devices does this person have?", so per-device targeting, per-device opt-out, and device-level delivery diagnostics are all out of reach.
Possible directions
No preference on shape, but each of these would address it:
- Include a device identifier in the property key (e.g.
$device_push_subscription_<app_id>_<device_id>), with the send path fanning out over matching keys. Smallest change, but person properties then carry an unbounded set. - Store a list rather than a scalar, with the SDK supplying a stable device id for dedupe and replacement.
- A first-class subscription model keyed on person + device, which would also give somewhere to hang platform, last-seen and unregister state, and make device state queryable.
Notes
Not currently listed under Known issues in #72251.
Verified against master as of 2026-08-26.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with products/messaging/backend/api/push_subscriptions.py and trace the property update into nodejs/src/ingestion/common/persons/person-update.ts. Then inspect nodejs/src/cdp/utils/push-subscription-utils.ts and buildFcmMessage to compare the available storage shapes with the send path. Done means same-platform devices remain addressable after registration and the chosen design supports the required targeting behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- firebase, python, typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100