Push Notifications (FCM / APNs via Expo)
Linear epic: FIT-170 Status: Shipped. Powers workout-assigned, new-message, new-comment, announcement, and class-reminder push. Last reviewed: 2026-05-28
What
Mobile push notification fan-out using Expo’s push service (which fronts FCM for Android and APNs for iOS). Device tokens are registered per user, kept current via last_seen_at, and soft-deleted on sign-out / invalid receipts. Send paths are queued via BullMQ with retry; receipt fetching happens 15min after send so we can clean up DeviceNotRegistered tokens.
Why
Real-time mobile engagement: a workout drop, a coach reply, a gym announcement, the T-30min class reminder. Push is the primary attention channel for active members.
Personas
| Persona | Surface | Capabilities |
|---|---|---|
| Member | Mobile app | Receives push; toggles per-category prefs |
| System (other modules) | API services | Call PushNotificationsService.notifyUser(s) |
Capabilities
- Token register / revoke —
POST /devices/registerupserts onexpo_push_token(refresh owner + lastSeen on re-install or sign-in);DELETE /devices/:tokenrevokes (caller-owns gated). - Categories —
workoutAssigned,newMessage,newComment,announcement,classReminder(canonical list in@fitkit/shared/lib/schemas/notification-prefs.ts). - Per-category × per-channel opt-out — JSONB column
notification_prefs.prefs, default opt-in. Channelspush,email. - Notification prefs API —
GET/PATCH /users/me/notification-preferences. - Fan-out APIs —
notifyUser(userId, payload),notifyUsers(userIds, payload),scheduleNotifyUser(userId, payload, sendAt)(with BullMQ delay). - Processor — chunks via
expo.chunkPushNotifications(max 100 per HTTP call), sends, collects ticket IDs, schedules a 15min-delayedcheck-receiptsfollow-up job. - Receipt processor — fetches by ticket id, soft-deletes tokens reporting
DeviceNotRegistered.
Capabilities (gaps)
- Web push not implemented (PWA infra exists but no service-worker fan-out).
- No per-org pref override (all prefs are per-user).
- No quiet hours / DND.
- Compliance form issuance does NOT push yet — see
../forms/README.mdgaps.
Related
../messages-comments/— callsnotifyUseron each new message/comment.../announcements/— callsnotifyUsersfor fan-out.../notifications/— parallel email channel.../scheduling-bookings/— class reminder T-30min usesscheduleNotifyUser.
Source code
- API:
apps/api/src/push-notifications/ - DB:
libs/db/src/lib/schema/device-tokens.ts,libs/db/src/lib/schema/notification-prefs.ts - Shared:
libs/shared/src/lib/schemas/notification-prefs.ts - Mobile client:
fitkit-mobile(separate repo) —src/hooks/use-push-notifications.tsowns permission prompt, Expo push token fetch, registration, foreground handler, and tap-to-route. Seedocs/architecture/mobile.mdfor the full lifecycle.