feat(notifications): mobile push foundation (APNs + FCM channels) (LC-91) #179
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-91-mobile-push-foundation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Server-side foundation for native mobile push (iOS APNs, Android FCM), alongside the existing Web Push. Scoped deliberately: the live APNs/FCM HTTP senders, operator-credential storage, and per-channel settings UI are deferred until the native client (LC-99 / LC-123) and Apple/Firebase credentials exist. This PR lands the storage, registration, and dispatch fan-out so that work plugs in without reshaping the notification path. Verified end-to-end with mocks.
Why scoped this way
LC-91 AC #1 needs "the native mobile client can register a device token", but LC-99 (mobile apps) is To-do and LC-123 (Tauri 2) is In-progress: there is no client to register tokens or receive pushes, and no APNs cert / FCM service account / device available here to test live delivery. The mockable trait-based foundation is the verifiable slice; it unblocks the client work and follows the codebase's existing
MockPushClientpattern.Changes
0022adds two narrow per-kind tables:apns_subscriptions(device_token+topic) andfcm_subscriptions(registration_token), keyed by the provider-issued token.push_subscriptions(Web Push) is untouched. New db modules mirror it (insert_or_replace,for_user,delete_by_token,bump_last_seen).push::dispatchnow fans out across every configured channel. The gates that decide whether to notify at all (notify_push_enabled, LC-88 DND, LC-90 per-room mute) run once up front so they apply uniformly to all channels; the payload is built once and shared, so the deep-link / title / body are identical across Web Push, APNs, FCM. Each channel spawns under the existing concurrency semaphore, and a dead token (EndpointGone= APNsBadDeviceToken/ FCMNOT_REGISTERED) is pruned inline, mirroring the Web Push 410 path.AppStategainsapns_client/fcm_clientasOption<Arc<dyn ...>>, bothNonein production until the live senders land. An unconfigured channel is skipped entirely: no send, and crucially no pruning, so a missing sender never looks like a dead token.ApnsClient/FcmClienttraits +MockApnsClient/MockFcmClientmirror thePushClientpattern.POST /push/apnsandPOST /push/fcm(authed) store a token for the user; accepted even before a sender is configured so a client can register early.notify_push_enabled.Acceptance criteria
notify_push_enabled).Deferred (live-wiring phase: needs client + credentials)
settings.db(AES-256-GCM, like SMTP).Test
just check,just test,just test-saasgreen. New: db round-trip tests for both tables;push_dispatchall-channel fan-out, APNs/FCM dead-token pruning, uniform suppression (notify-disabled / muted-all / DND), and a no-op-when-unconfigured guard. Migration0022added to every test pool helper. Theroutes_uploadsupload-pipeline flake under concurrent-binary load is pre-existing (CLAUDE.md) and passes in isolation.🤖 Generated with Claude Code