Summary
On my instance, poll(-expiry) notifications appear in GET /api/v1/notifications but never in GET /api/v2/notifications (the grouped feed). This makes poll notifications invisible to clients that use the v2 grouped API, and it produces a permanent "new notifications" badge in Phanpy. I've tried to trace the cause below, but I'm not certain of the code-level explanation — I may be missing an intended code path or a reason it's designed this way, so please treat the "likely cause" section as a hypothesis to confirm rather than a definitive diagnosis.
Observations (these are directly verifiable on my instance, independent of the cause)
notifications table: 0 rows of type poll. notification_groups table: 0 rows of type poll — while the polls table contains several expired polls.
GET /api/v1/notifications?limit=1 returns a poll notification, e.g. id = "2026-06-06T16:40:20.000Z/poll/poll-<uuid>", that is newer than
GET /api/v2/notifications?limit=1, whose newest group is most_recent_notification_id = "2026-06-05T16:06:50.122Z/follow/<uuid>" (type follow).
GET /api/v1/markers?timeline[]=notifications → last_read_id equals that v2 follow id exactly. So the marker is being written/stored correctly; it just reflects the v2 feed, which never contains the poll.
For comparison, the non-poll types behave consistently: notification_groups tracks notifications as expected (ungrouped types equal; favourite/follow/reblog collapsed by grouping). So grouping/materialization itself seems fine — the anomaly is specific to poll notifications.
Likely cause (hypothesis — please confirm)
From reading the 0.9.2 source, it appears that:
- Poll notifications are produced for the v1 feed by an on-the-fly synthesis from the
polls table (src/api/v1/notifications.ts, ~L226–302), rather than from stored notification rows.
GET /api/v2/notifications serves only from the materialized notification_groups table (src/api/v2/notifications.ts), so anything not materialized there cannot appear in v2. poll is, however, listed among v2's default returnable types (src/api/v2/notifications.ts:76).
- There is a
createPollNotifications() in src/notification.ts that would materialize a real poll notification + group, but I couldn't find any call site for it in the tree (its siblings like createFollowNotification are called from src/federation/inbox.ts; this one doesn't appear to be), nor a poll-expiry scheduler that would invoke it. I could easily be wrong about this — if poll notifications are intentionally v1-only, or are materialized via a path I missed, please disregard this part.
If that reading is correct, the net effect is that v1 and v2 disagree about which notifications exist, specifically for polls.
Downstream symptom: permanent unread badge in Phanpy
As far as I can tell from Phanpy's source, it (in grouped mode) writes its read marker from the v2 feed but polls the v1 feed and flags "new" when v1_newest_id !== marker_id (strict inequality). Because v1's newest item can be a poll that v2 never returns, the marker can't advance to it, so the badge stays on. This may be partly a Phanpy-side fragility too (comparing across two id sources) — I'm reporting it here because the underlying v1/v2 divergence originates server-side.
Why this might not reproduce on an active instance (also a hypothesis)
The badge symptom seems to require an expired poll to be the account's newest notification and stay that way. On a quiet single-user instance that follows poll-posting accounts, there's often a freshly-expired poll at the v1 head, so it persists. On a busier instance, a newer materialized notification (mention/follow/…) would quickly become the v1 head — and those are in v2 — so the marker would advance and the badge would clear, hiding the issue. This is my best guess at why it may not have reproduced for you.
Possible directions (deferring to your judgment)
- If poll notifications are meant to be materialized: invoke
createPollNotifications at poll expiry so they land in notifications / notification_groups like other types (the function already exists), after which the v1 synthesis could be retired.
- Otherwise: include the synthesized poll notifications in the v2 response as single-item groups, so v1 and v2 agree.
Environment
- Hollo 0.9.2
GET /api/v2/instance → api_versions: { mastodon: 7 }
- PostgreSQL 13 with some local compatibility workarounds. I checked whether my setup could be responsible and don't think it is (grouping works for all materialized types; the one custom aggregate I run,
any_value, is only used in two unrelated migrations, not the notification path) — but I mention it in case it's relevant.
Summary
On my instance, poll(-expiry) notifications appear in
GET /api/v1/notificationsbut never inGET /api/v2/notifications(the grouped feed). This makes poll notifications invisible to clients that use the v2 grouped API, and it produces a permanent "new notifications" badge in Phanpy. I've tried to trace the cause below, but I'm not certain of the code-level explanation — I may be missing an intended code path or a reason it's designed this way, so please treat the "likely cause" section as a hypothesis to confirm rather than a definitive diagnosis.Observations (these are directly verifiable on my instance, independent of the cause)
notificationstable: 0 rows of typepoll.notification_groupstable: 0 rows of typepoll— while thepollstable contains several expired polls.GET /api/v1/notifications?limit=1returns apollnotification, e.g.id = "2026-06-06T16:40:20.000Z/poll/poll-<uuid>", that is newer thanGET /api/v2/notifications?limit=1, whose newest group ismost_recent_notification_id = "2026-06-05T16:06:50.122Z/follow/<uuid>"(typefollow).GET /api/v1/markers?timeline[]=notifications→last_read_idequals that v2followid exactly. So the marker is being written/stored correctly; it just reflects the v2 feed, which never contains the poll.For comparison, the non-poll types behave consistently:
notification_groupstracksnotificationsas expected (ungrouped types equal; favourite/follow/reblog collapsed by grouping). So grouping/materialization itself seems fine — the anomaly is specific to poll notifications.Likely cause (hypothesis — please confirm)
From reading the 0.9.2 source, it appears that:
pollstable (src/api/v1/notifications.ts, ~L226–302), rather than from stored notification rows.GET /api/v2/notificationsserves only from the materializednotification_groupstable (src/api/v2/notifications.ts), so anything not materialized there cannot appear in v2.pollis, however, listed among v2's default returnable types (src/api/v2/notifications.ts:76).createPollNotifications()insrc/notification.tsthat would materialize a realpollnotification + group, but I couldn't find any call site for it in the tree (its siblings likecreateFollowNotificationare called fromsrc/federation/inbox.ts; this one doesn't appear to be), nor a poll-expiry scheduler that would invoke it. I could easily be wrong about this — if poll notifications are intentionally v1-only, or are materialized via a path I missed, please disregard this part.If that reading is correct, the net effect is that v1 and v2 disagree about which notifications exist, specifically for polls.
Downstream symptom: permanent unread badge in Phanpy
As far as I can tell from Phanpy's source, it (in grouped mode) writes its read marker from the v2 feed but polls the v1 feed and flags "new" when
v1_newest_id !== marker_id(strict inequality). Because v1's newest item can be a poll that v2 never returns, the marker can't advance to it, so the badge stays on. This may be partly a Phanpy-side fragility too (comparing across two id sources) — I'm reporting it here because the underlying v1/v2 divergence originates server-side.Why this might not reproduce on an active instance (also a hypothesis)
The badge symptom seems to require an expired poll to be the account's newest notification and stay that way. On a quiet single-user instance that follows poll-posting accounts, there's often a freshly-expired poll at the v1 head, so it persists. On a busier instance, a newer materialized notification (mention/follow/…) would quickly become the v1 head — and those are in v2 — so the marker would advance and the badge would clear, hiding the issue. This is my best guess at why it may not have reproduced for you.
Possible directions (deferring to your judgment)
createPollNotificationsat poll expiry so they land innotifications/notification_groupslike other types (the function already exists), after which the v1 synthesis could be retired.Environment
GET /api/v2/instance→api_versions: { mastodon: 7 }any_value, is only used in two unrelated migrations, not the notification path) — but I mention it in case it's relevant.