Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesPush NotificationsPush Notifications (FCM / APNs via Expo)

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

PersonaSurfaceCapabilities
MemberMobile appReceives push; toggles per-category prefs
System (other modules)API servicesCall PushNotificationsService.notifyUser(s)

Capabilities

  • Token register / revokePOST /devices/register upserts on expo_push_token (refresh owner + lastSeen on re-install or sign-in); DELETE /devices/:token revokes (caller-owns gated).
  • CategoriesworkoutAssigned, 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. Channels push, email.
  • Notification prefs APIGET/PATCH /users/me/notification-preferences.
  • Fan-out APIsnotifyUser(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-delayed check-receipts follow-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.md gaps.

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.ts owns permission prompt, Expo push token fetch, registration, foreground handler, and tap-to-route. See docs/architecture/mobile.md for the full lifecycle.