Repository navigation
Bring back push on Chrome and Android devices the server switched off #417
Description
Activity
- added a parent issue
on Oct 6, 2026 Fix is on branch
claude/youthful-lovelace-3nvsc6:da0d832server: S1–S3ea384b6client and service worker: C1–C4, W1–W404fa4c0database: D1–D2, with migration20261006120000_push_online_at_and_preference_checks.sqland the regeneratedseed.sql
Every unit passed its adviser, deslop and verify stages. The code-janitor supervisor granted the production-ready flag on round 2; round 1 asked only to regenerate
seed.sql. The full pre-push check passed: lint, format, security, typecheck, webapp Jest, backend tests, and the webapp and adminbuild:ci.Deploy order:
- Apply the migration.
- Deploy the apps, including Stop the SSRF filter from treating fcm.googleapis.com as a private address #398 (
0f6fc54). - Run the recovery SQL from the runbook above.
Not yet checked on real devices: Safari and Android push end to end. Use the runbook's step 3.
Generated by Claude Code
- added a commit that references this issue
on Oct 6, 2026 - changed the title
[-]PWA push: Chrome and Android devices stay switched off, and nothing brings them back[/-][+]Bring back push on Chrome and Android devices the server switched off[/+]on Oct 6, 2026 Facts the comment above does not record:
The recovery SQL is not in the repo. Runbook step 2 points at
Notes/local-docs/push-review-2026-10-06-server.md, which is gitignored. Queries B and C are copied here. Replace<FIX_DEPLOYED_AT>with the deploy time of0f6fc54. Run them soon: a cron job deletes rows that stay switched off for 30 days. Run them only after0f6fc54is live. First confirm the worker logs no newRefused an unsafe push endpointline for an FCM row.-- B. Read-only. The rows the update will change. Compare the count with C. select ps.id, ps.user_id, ps.device_name, ps.platform, ps.updated_at from public.push_subscriptions ps where ps.is_active = false and ps.last_error = 'Subscription expired or invalid' and ps.push_credentials->>'endpoint' ~* '^https://fcm\.googleapis\.com/' and ps.updated_at >= timestamptz '2026-08-09 13:43:49+00' -- commit 6d19d16 and ps.updated_at < timestamptz '<FIX_DEPLOYED_AT>' and not exists ( select 1 from public.push_subscriptions a where a.user_id = ps.user_id and a.is_active and a.push_credentials->>'endpoint' = ps.push_credentials->>'endpoint') order by ps.updated_at; -- C. The update. Same filter as B. Check the returned count, then commit or roll back. begin; update public.push_subscriptions ps set is_active = true, failed_count = 0, last_error = null where ps.is_active = false and ps.last_error = 'Subscription expired or invalid' and ps.push_credentials->>'endpoint' ~* '^https://fcm\.googleapis\.com/' and ps.updated_at >= timestamptz '2026-08-09 13:43:49+00' and ps.updated_at < timestamptz '<FIX_DEPLOYED_AT>' and not exists ( select 1 from public.push_subscriptions a where a.user_id = ps.user_id and a.is_active and a.push_credentials->>'endpoint' = ps.push_credentials->>'endpoint') returning ps.id, ps.user_id; -- commit; -- or: rollback;
A dead endpoint gets 404 or 410 at its next send and is switched off again. That is the correct result.
Migrations go up by hand. Supabase SQL has no deploy pipeline. Apply
20261006120000_push_online_at_and_preference_checks.sqlbefore the app deploy. The branch also carries20261006130000_close_client_write_and_grant_gaps.sql(#397, #401, #409, #410).D2 checks new writes only. Values stored before the migration are not checked again. A bad stored value can still abort a
messagesinsert.C2 depends on a local stamp. The client repairs a missing browser subscription only when
localStorageholdsdocsplus_push_subscription_timestamp. The stamp is the opt-in record. No subscription and no stamp counts as "opted out".
Generated by Claude Code
- added 4 commits that reference this issue
on Oct 9, 2026 Landed on
main, not deployed yetReopened: the board automation closed this issue before the push. It stays open until the prod steps are done.
The branch merged into
mainas083f37d84, with no rebase, soda0d832,ea384b6and04fa4c0above still resolve. Follow-ups on top:946238c61: a row that the URL filter refuses no longer makes the push job retry and dead-letter. A bad subscription key now adds 1 tofailed_count. Sign-out waits for an in-flight push sync.2a2f5c99f: the service worker no longer handlespushsubscriptionchange. The next signed-in load repairs a changed subscription instead.9e00856ecand62c9211a1: smaller tidy-ups of the same code, with no behavior change.
Prod steps:
- Apply migration
20261006120000_push_online_at_and_preference_checks.sql. - Deploy the backend and the webapp.
- Run the recovery SQL (queries B, then C, in the comment above). Do it soon: a cron job deletes rows that stay switched off for 30 days.
- Check Safari and Android push end to end.
Summary
Web push does not reach Chrome, Edge, Android, Brave, Opera or Samsung Internet users in production. Only Firefox and Safari (macOS and iOS Home Screen apps) receive push today.
6d19d16(2026-08-09) made the SSRF filter refusefcm.googleapis.com. The sender then switched each FCM subscription off at its first push, with the same text a real 404/410 writes. The fix is Stop the SSRF filter from treating fcm.googleapis.com as a private address #398 (commit0f6fc54), not deployed yet.Notes/local-docs/push-review-2026-10-06-client.mdand-server.md).Production data (2026-10-06, read-only)
last_error = 'Subscription expired or invalid', all after 2026-08-09.push_notifications: 0 waiting; 5 messages archived in 21 days.Scope of the fix
Server —
apps/hocuspocus.server/src/lib/push/sender.tssender.ts:159-162into:70-76).TTL(1 day) and a requesttimeout(10 s). Today there is no TTL (4-week default) and no timeout (sender.ts:176-182).failed_count. Store the status code inlast_error, for exampleHTTP 403, soget_push_failure_summarycan group it.Client —
apps/webappregister_push_subscriptionupserts). Remove the 30-day unsubscribe-then-subscribe path, which can destroy a working subscription (src/utils/push-notifications.ts:321-382). Share one in-flight promise, because the prompt card and Settings both mount the hook.src/components/NotificationPromptCard.tsx:107-109,src/hooks/usePushNotifications.ts).unregister_push_subscriptionandsubscription.unsubscribe()beforesignOut()(src/components/settings/hooks/useSignOut.ts:12-30). Today a shared device keeps showing the previous user's messages.applicationServerKeydiffers fromNEXT_PUBLIC_VAPID_PUBLIC_KEY, subscribe again (src/utils/push-notifications.ts:233-236).Service worker —
apps/webapp/public/service-worker.jspushsubscriptionchange: subscribe again with the same key. C1 then saves it on the next load.message,content_change,system_alert,invitation(service-worker.js:87-104againstpackages/supabase/scripts/01-enum.sql:71-81).service-worker.js:140-152). Chrome otherwise shows its own text and may revoke the subscription.notificationclickopens same-origin paths only; any other origin becomes/(service-worker.js:183-211).Database — new migration plus
packages/supabase/scripts/mirrorupdate_user_online_atstampsonline_atonly whenstatuschanges, so the 60 s heartbeat never refreshes it. Users in an open tab are treated as offline after 2 minutes, and users stuck atONLINElose regular-message push. Stamponline_aton every status write.update_notification_preferencesaccepts unchecked values that the push trigger later casts (::boolean,::time,at time zone). One bad value aborts themessagesinsert for the whole channel. Validate the known keys and types in the RPC.Out of scope (deliberately, to avoid overengineering)
Per-device online suppression, a new Android badge asset, iPhone and Android device-name parsing, a deactivation metric and alert, VAPID in
/health, purgingpgmq.a_push_notifications, a test-push button, and deploy-time key matching in compose. File them separately if wanted.Runbook after merge (maintainer)
0f6fc54). Note the deploy time.Notes/local-docs/push-review-2026-10-06-server.md(queries A, B, C), with<FIX_DEPLOYED_AT>set to the deploy time:is_active = true, failed_count = 0, last_error = nullfor those rows, in one transaction. Compare the count with B, then commit.pgmq.a_push_notificationsand a notification on the device.Acceptance criteria
failed_count. A 404/410 still switches the row off.online_atfresh, and regular-message push still reaches devices of users who left.update_notification_preferences, and sending messages never fails because of preferences.Related
#398 (the FCM filter fix).
Generated by Claude Code