PostHog / PostHog/posthog

Push: one device token per person per app — a second same-platform device silently replaces the first

Open
#89,059 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

feature/push-notifications team/workflows
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:

  1. 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.
  2. Store a list rather than a scalar, with the SDK supplying a stable device id for dedupe and replacement.
  3. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.