Push Notifications
How the app sends alerts to phones -device tokens, what triggers a push, user preferences, and deep links.
Last updated Thu Jul 16 2026 00:00:00 GMT+0000 (Coordinated Universal Time)
TL;DR -Push notifications are the alerts that pop up on a phone even when the app is closed ("a host you follow just went live"). They ride on Google's FCM, are best-effort (a failed push never blocks anything), and can be turned off per category by each user. If push credentials aren't configured, the whole system quietly disables itself.
Who this is for -Product & Business: read "What push is" and the triggers table to see what alerts users get. Engineering, Mobile & DevOps: configuration, the token registry, and deep-link routing.
What push is
A push notification is a message the backend sends to a user's phone that shows up even when the app isn't open. In Social Live these are best-effort nudges -"someone messaged you", "a host went live" -layered on top of the durable in-app notification list.
The distinction matters: the in-app notification (a row in the database) is the permanent record; the push is just a delivery attempt to the device. If the push never lands, the in-app row is still there next time the user opens the app.
Everything flows through FCM (Firebase Cloud Messaging,
firebase-admin) -Google's delivery service, which also relays to Apple's
APNs for iPhones, so one integration covers both platforms.
Push complements the in-app notifications module: the in-app row is the
durable record, the push is best-effort delivery to devices. Everything
flows through FCM (firebase-admin), which also fronts APNs for iOS.
Configuration
Push only works when the right credentials are present -and it's designed so that when they're missing, nothing breaks; push just goes silent. That's what lets developers run the app locally without any Firebase setup.
- Backend:
FIREBASE_SERVICE_ACCOUNT_BASE64-base64-encoded Firebase service-account JSON. Unset = push disabled; every send becomes a silent no-op (PushService.enabled === false), so dev environments work without credentials. - iOS client: add
GoogleService-Info.plisttoios/Runner, upload the APNs auth key in the Firebase console. Theaps-environmententitlement andremote-notificationbackground mode are already in the project. - Android client: drop
google-services.jsonintoandroid/app/-the google-services Gradle plugin only applies when that file exists, so builds without it keep working. - The Flutter side initializes Firebase defensively: no config files → push silently disabled, app unaffected.
Device token registry
To send a push, FCM needs a device token -a unique address for one app install on one phone. The backend keeps a registry of these and manages the tricky cases (shared phones, rotated tokens, dead tokens).
DeviceToken rows (one per install): POST /notifications/devices
{ token, platform: ios|android } upserts by token -a token already
registered to another account is reassigned to the signing-in user, so
shared devices never push to the previous account.
DELETE /notifications/devices/:token on sign-out. Tokens FCM reports as
dead (registration-token-not-registered) are pruned after each send.
The client registers after sign-in (PushNotificationsGate watches the
auth session), re-registers on FCM token rotation, and unregisters on
sign-out.
Triggers
These are the events that actually cause a push to be sent, along with the preference category that governs each and where a tap takes the user.
| Event | Category | Deep link |
|---|---|---|
Followed host goes live (goLive) | liveStart | /live/:roomId |
New DM (chat.sendMessage) | directMessages | /inbox/:conversationId |
| New follower / follow request / request accepted | followActivity | /notifications |
All sends are fire-and-forget -a push failure never blocks the triggering request. Live-start fans out to all followers in FCM batches of 500, with opted-out users filtered in bulk.
Preferences
Users control which of these categories they receive. A missing preferences row means everything is on by default.
NotificationPreference (one row per user, missing row = all on):
liveStart, directMessages, followActivity.
GET /notifications/preferences, PATCH /notifications/preferences
{ liveStart?, directMessages?, followActivity? }. In the app:
Profile → Settings → Push notifications.
Deep links
A deep link is the instruction that tells the app which screen to open when a user taps a notification -so a "new DM" push opens the conversation, not just the home screen.
Payload data carries type (live | dm | follow) plus ids; the
client routes taps (background and cold start via
getInitialMessage) through go_router.
Related pages
- Backend Module Map -the
notificationsmodule that owns the in-app feed and device registry. - Realtime (Socket.IO) -in-app real-time updates, the counterpart to out-of-app push.
- System Architecture -where FCM sits among the external services.