Notifications — Technical Reference
Where this lives
Backend
apps/backend/graphql/resolvers/notification.resolver.js—notification,myNotifications,unreadNotificationsCount,createNotification,markNotificationAsRead,markAllNotificationsAsRead,deleteNotification,deleteAllNotifications,notificationAddedsubscriptionapps/backend/graphql/resolvers/user-notifications.resolver.js—notificationSettings,updateNotificationSettings,resetNotificationSettingsapps/backend/graphql/resolvers/user-sessions.resolver.js—registerDeviceTokenapps/backend/graphql/resolvers/call.resolver.js—registerVoIPToken,unregisterVoIPToken(used for incoming call push)apps/backend/graphql/types/notification.type.js—NotificationSDL type and enumapps/backend/managers/user-managers/notification.manager.js— notification business logic, grouping, andnotificationAddedpubsub publishapps/backend/data-access-services/user/notification.access-service.js— notification DB accessapps/backend/services/notification-queue.service.js— BullMQ queue that backsnotification.worker.jsapps/backend/services/message-notification.service.js— routes new-message events into the notification/push pipelineapps/backend/services/notification/notification.service.js— thin wrapper that hands off to theservices/push-notification/facadeapps/backend/services/push-notification/push-notification.service.js— provider-agnostic push facade (PUSH_NOTIFICATION_PROVIDERenv var selects the provider, defaultfirebase);providers/firebase.provider.jsis the current (only) providerapps/backend/services/firebase.service.js— Firebase Admin SDK init, shared by push delivery and Apple-auth token verificationapps/backend/services/apns.service.js— backward-compatible entry point that now just delegates to thepush-notificationfacade (regular iOS/Android/web push all go through FCM; it no longer talks to APNs directly)apps/backend/services/voip-push.service.js— separate, real APNs (@parse/node-apn) integration used only for VoIP push on incoming calls (used bycall.manager.js/call.resolver.js)
Frontend
apps/frontend-nextjs/src/app/notifications/page.tsx— notifications page routeapps/frontend-nextjs/src/components/notifications/NotificationsPanel.tsx— notification list/dropdownapps/frontend-nextjs/src/components/notifications/NotificationsButton.tsx— bell icon with unread badgeapps/frontend-nextjs/src/page-components/settings/NotificationsSettingsPage.tsx— per-channel settings UI
Technical implementation checklist
-
myNotifications/markNotificationAsRead/markAllNotificationsAsRead— resolvers wired innotification.resolver.js;NotificationsPanel.tsxqueries, displays, and marks items read -
firebase.service.js/push-notification/facade /notification-queue.service.js— fully implement server-side push delivery;WebPushRegistrar.tsx(mounted once for authenticated users, next toUsageHeartbeat) now callsregisterDeviceTokenwithplatform: 'web'wheneverinitWebPush()resolves a token, so push is actually delivered to the web client once Firebase web config is present (previously the frontend never called this mutation) -
notificationSettings/updateNotificationSettings/resetNotificationSettings— resolvers wired inuser-notifications.resolver.js;NotificationsSettingsPage.tsxqueries and mutatesnotificationSettings(email/push/SMS channels) directly, and separately queries/mutates apauseAll/quietHoursEnabled/messagesOnlyModeslice ofextendedNotificationSettingsfor its hub toggles — real GraphQL calls, not static switches -
unreadNotificationsCount/notificationAddedsubscription —NotificationsButton.tsxandNotificationsPanel.tsxsubscribe tonotificationAddedand refetch the count/list on each event, replacing the previous 30-second polling with push-driven updates
Notification types
The enum has grown well beyond the original social set to cover commerce, moderation, and account-security events:
enum NotificationType {
like # Reaction on your post
comment # Comment on your post
follow # New follower
follow_request # Someone requested to follow you (private account)
mention # Mentioned in a comment or message
tag # Tagged in a post
message # New direct message
message_purchase # Someone bought a paid message
post # Post from someone you follow
post_purchase # Someone bought a paid post
repost # Your post was shared
story_like # Reaction on your story
reaction # Reaction on your story (alt path)
subscription # New subscription to your profile
subscription_renewal_reminder # Upcoming subscription renewal
conversation_subscription # New paid-chat subscription
tip # You received a tip
cashout # Cashout status update
product_purchase # Someone bought your product
order_shipped # Order shipped
order_canceled # Order canceled
dispute_opened # Payment dispute opened
dispute_resolved # Payment dispute resolved
identity_verification_approved # ID verification approved
identity_verification_rejected # ID verification rejected
referral_bonus # Referral bonus earned
collaboration_invite # Invited to collaborate on a post
collaboration_accepted # Collaboration invite accepted
collaboration_rejected # Collaboration invite declined
collaboration_removed # Removed from a collaboration
security_issue # Security alert
system # System notification
system_alert # System notification (alt path)
new_report # New report filed (moderation)
moderation_action # Account moderation action taken
}
DEFAULT_PUSH_TITLES in notification.manager.js maps each type to its push notification title. Social types in GROUPABLE_PUSH_TYPES (like, comment, follow, follow_request, repost, story_like, reaction, tag) have push bursts collapsed into one notification per 5-minute window; transactional/message/security types are never grouped.
Notification model
| Field | Description |
|---|---|
notificationType | Type (see enum) |
actorId / actor | Who triggered the notification |
entityType / entityId | Related content |
message | Notification text |
isRead / readAt | Read state |
groupKey / groupCount | Grouping (e.g. "5 likes") |
metadata | Extra data (JSON) |
Queries
myNotifications returns the current user's notification list, newest first. Notifications with the same groupKey are grouped — groupCount tells you how many events collapsed into one item (e.g. "Alice, Bob and 3 others liked your post"). Each item also resolves a post field (thumbnail for post-related notifications) and an actor field, both resolved on demand if not eager-loaded.
notification(id) fetches a single notification by ID (authenticated, ownership-checked in the manager).
unreadNotificationsCount is a cheap counter — call it on app mount; the frontend keeps it fresh afterward via the notificationAdded subscription rather than polling.
notificationSettings returns the user's per-channel toggle state — useful for rendering the settings UI.
query MyNotifications($limit: Int, $offset: Int) {
myNotifications(limit: $limit, offset: $offset) {
id notificationType message isRead createdAt
actor { username profilePicture }
entityType entityId
groupKey groupCount
post { id media { thumbnailUrl mediaUrl } }
}
}
query UnreadCount { unreadNotificationsCount }
query NotificationSettings {
notificationSettings {
emailNotifications pushNotifications smsNotifications
likesNotifications commentsNotifications followsNotifications
mentionsNotifications messagesNotifications postsNotifications
marketingNotifications securityNotifications
}
}
Mutations
markNotificationAsRead marks a single item read and updates readAt. markAllNotificationsAsRead is a bulk version — call it when the user opens the notification panel.
deleteNotification permanently removes one item. deleteAllNotifications clears the entire history.
createNotification creates a notification for an arbitrary userId — intended for system/admin use, not exposed in the regular UI.
updateNotificationSettings persists channel toggles. resetNotificationSettings reverts all toggles to platform defaults.
mutation MarkRead($notificationId: ID!) { markNotificationAsRead(notificationId: $notificationId) }
mutation MarkAllRead { markAllNotificationsAsRead }
mutation DeleteNotification($id: ID!) { deleteNotification(notificationId: $id) }
mutation DeleteAllNotifications { deleteAllNotifications }
mutation UpdateNotificationSettings($input: NotificationSettingsInput!) {
updateNotificationSettings(input: $input) { success }
}
mutation ResetNotificationSettings { resetNotificationSettings }
Subscription
notificationAdded(userId: ID!) fires for the recipient each time a new notification is created for them. NotificationsButton.tsx and NotificationsPanel.tsx subscribe to it (scoped to the logged-in user's own ID) and refetch the unread count / list on each event — this replaced the previous 30-second poll interval.
subscription OnNotificationAdded($userId: ID!) {
notificationAdded(userId: $userId) { id }
}
Per-channel settings
Each channel is toggled independently:
| Setting | Description |
|---|---|
emailNotifications | Email delivery |
pushNotifications | FCM push delivery |
smsNotifications | SMS delivery |
likesNotifications | Reactions on posts |
commentsNotifications | Comments on posts |
followsNotifications | New followers |
mentionsNotifications | Mentions in content |
messagesNotifications | Direct messages |
postsNotifications | Posts from followed users |
marketingNotifications | Platform news and updates |
securityNotifications | Security alerts |
Push notifications (FCM)
The backend sends push notifications via Firebase Cloud Messaging, through a provider-agnostic facade (services/push-notification/) that both apns.service.js and services/notification/notification.service.js delegate to:
- On login (web) or app launch (iOS), the frontend registers the device token (
registerDeviceToken) — on web this isWebPushRegistrar.tsx, gated on Firebase web config notification.worker.jsruns as a background BullMQ job (concurrency 10, up to 100/sec) to dispatch pushes- The backend uses the Firebase Admin SDK, initialized in
firebase.service.jsfrom theFIREBASE_PROJECT_ID/FIREBASE_PRIVATE_KEY/FIREBASE_CLIENT_EMAILenv vars PUSH_NOTIFICATION_PROVIDER(defaultfirebase) selects the delivery provider in the facade;DISABLE_PUSH_NOTIFICATIONSshort-circuits all sends to mock responses (used in dev/test)
For iOS, VoIP push for incoming calls is handled separately via real APNs (@parse/node-apn in voip-push.service.js, configured with APNS_PRIVATE_KEY / APNS_KEY_ID / APNS_TEAM_ID / APNS_BUNDLE_ID) rather than through the FCM facade — see registerVoIPToken.
Extended settings
extendedNotificationSettings / updateExtendedNotificationSettings (graphql/types/extended-notification-settings.type.js + resolver) is a separate, much larger surface layered on top of the base channels above — full detail lives in Settings → Notifications since it was built as part of the Settings feature pass rather than this one. Backed by notification-settings.manager.js, which had far more internal keys than were ever exposed via the base NotificationSettings type — most of the extended fields are just newly-exposed pre-existing manager state, not new business logic.