You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On the self-hosted/dev setup, push notifications only appear while the app is open; nothing arrives when it's closed. Root cause is not the "app closed" case at all — it's that no Expo push token is ever registered on the server, so the daemon has no one to push to. The notifications seen while open come from a different, foreground-only code path, which masks the broken remote-push path.
Architecture: two independent notification paths
Path
Mechanism
Works when app…
In-app (local)
UI renders a local notification from a live websocket event (activityLocalNotificationBus → sendExpoLocalNotification)
OPEN only — socket disconnects on background (apps/ui/sources/sync/sync.ts:945, apiSocket.disconnect())
Server logs show only GET/v1/push-tokens (daemon polling); zero POST (app registration) in window.
Aside (real, intermittent, now mitigated): /v1/push-tokens historically 502 / 15s-timeout repeatedly (daemon log [PUSH] Failed to fetch push tokens: timeout of 15000ms exceeded + Skipping push token fetch during failure cooldown {remainingMs:~27000}). DB was 1.1 GB, SessionMessage = 96.1%. Ran ANALYZE+PRAGMA optimize (backup pre-analyze-2026-06-20T17-01-11Z.bak); queries now ~3 ms.
Client registration path (apps/ui/sources/sync/engine/account/syncAccount.ts:204-332, triggered via InvalidateSync at sync.ts:897-903): registers with all server profiles. It has four silent abort points — any abort before the POST → no token anywhere:
Firebase config not baked — present: APK resources contain google_app_id / gcm_defaultSenderId and the publicdev app id 1:427065718939:android:fc6fcb80… (project happier-4a4fd).
EAS wiring — easProjectId 2a550bd7-e4d2-4f59-ab47-dcb778775cee, expo-notifications plugin, googleServicesFile all present and package-matched.
Prime suspect
With module + Firebase + permission + wiring all confirmed good, the only remaining failure point is the runtime getExpoPushTokenAsync() call. Most likely: the EAS project 2a550bd7… has no Android FCM V1 service-account key for dev.happier.app.publicdev. On Android, getExpoPushTokenAsync() calls Expo's backend, which requires FCM credentials; absent → it throws → registration aborts before POST → server stays at 0 tokens.
Verification (either confirms)
Device: in-app Logs / Notifications → Troubleshooting, search push token → expect Failed to get Expo push token: ….
Primary: upload/assign the Android FCM V1 service-account key to the EAS project (no app rebuild needed — server-side credential). Re-verify a token lands in AccountPushToken and a test push reaches a closed app. (Check APNs key for iOS too.)
Daemon resilience (apps/cli/src/api/pushNotifications.ts): cache last-known tokens, retry on fetch failure, shorten/skip the 30s cooldown for token fetch so a transient server stall doesn't drop notifications.
Server DB retention: prune/retain SessionMessage (96% of DB) to prevent the recurring slow-query/502 cascade.
Separate:happier-daemon.service (CT 106) is crashlooping (OOM, ~15 G peak vs MemoryMax) while sessions run under an older root happy-coder daemon — needs its own look.
Acceptance
AccountPushToken populates after app login.
Push received with app fully closed.
[PUSH] Failed to fetch push tokens no longer recurs under normal load.
Investigation by Claude Code (read-only repo exploration + live MCP inspection of CT 106/107 + published dev APK).
Summary
On the self-hosted/dev setup, push notifications only appear while the app is open; nothing arrives when it's closed. Root cause is not the "app closed" case at all — it's that no Expo push token is ever registered on the server, so the daemon has no one to push to. The notifications seen while open come from a different, foreground-only code path, which masks the broken remote-push path.
Architecture: two independent notification paths
activityLocalNotificationBus→sendExpoLocalNotification)apps/ui/sources/sync/sync.ts:945,apiSocket.disconnect())dispatchActivityNotification→PushNotificationClient.sendToAllDevicesAsync,apps/cli/src/api/pushNotifications.ts)App open → local path fires → user sees it. App closed → only remote push can fire → it can't.
Evidence
Server (CT 107,
happier.tail6018de.ts.net, serves/v1+/v2):AccountPushTokentable = 0 rows, 1 account → daemon fetches an empty recipient list./v1/push-tokens(daemon polling); zero POST (app registration) in window./v1/push-tokenshistorically 502 / 15s-timeout repeatedly (daemon log[PUSH] Failed to fetch push tokens: timeout of 15000ms exceeded+Skipping push token fetch during failure cooldown {remainingMs:~27000}). DB was 1.1 GB,SessionMessage= 96.1%. RanANALYZE+PRAGMA optimize(backuppre-analyze-2026-06-20T17-01-11Z.bak); queries now ~3 ms.Client registration path (
apps/ui/sources/sync/engine/account/syncAccount.ts:204-332, triggered viaInvalidateSyncatsync.ts:897-903): registers with all server profiles. It has four silent abort points — any abort before the POST → no token anywhere:loadExpoNotifications()throws → "Push notifications unavailable"getExpoPushTokenAsync()throws → "Failed to get Expo push token"apps/ui/sources/sync/api/session/apiPush.ts→POST /v1/push-tokens)Zero POSTs + 0 tokens ⇒ aborts before the POST.
Eliminated suspects (verified):
okhttpsockets).happier-dev-android.apk, buildpublicdev/dev.happier.app.publicdev) dex containsexpo-notifications(7 hits) +FirebaseMessaging(7); sherpa control = 0.google_app_id/gcm_defaultSenderIdand the publicdev app id1:427065718939:android:fc6fcb80…(projecthappier-4a4fd).easProjectId 2a550bd7-e4d2-4f59-ab47-dcb778775cee,expo-notificationsplugin,googleServicesFileall present and package-matched.Prime suspect
With module + Firebase + permission + wiring all confirmed good, the only remaining failure point is the runtime
getExpoPushTokenAsync()call. Most likely: the EAS project2a550bd7…has no Android FCM V1 service-account key fordev.happier.app.publicdev. On Android,getExpoPushTokenAsync()calls Expo's backend, which requires FCM credentials; absent → it throws → registration aborts before POST → server stays at 0 tokens.Verification (either confirms)
push token→ expectFailed to get Expo push token: ….eas credentials -p android(profilepublicdev) → "Push Notifications (FCM V1)" assigned, or "None".Fixes
AccountPushTokenand a test push reaches a closed app. (Check APNs key for iOS too.)apps/cli/src/api/pushNotifications.ts): cache last-known tokens, retry on fetch failure, shorten/skip the 30s cooldown for token fetch so a transient server stall doesn't drop notifications.SessionMessage(96% of DB) to prevent the recurring slow-query/502 cascade.happier-daemon.service(CT 106) is crashlooping (OOM, ~15 G peak vs MemoryMax) while sessions run under an older roothappy-coderdaemon — needs its own look.Acceptance
AccountPushTokenpopulates after app login.[PUSH] Failed to fetch push tokensno longer recurs under normal load.Investigation by Claude Code (read-only repo exploration + live MCP inspection of CT 106/107 + published dev APK).