Social LiveDocumentationPLATFORM DOCS

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.plist to ios/Runner, upload the APNs auth key in the Firebase console. The aps-environment entitlement and remote-notification background mode are already in the project.
  • Android client: drop google-services.json into android/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.

EventCategoryDeep link
Followed host goes live (goLive)liveStart/live/:roomId
New DM (chat.sendMessage)directMessages/inbox/:conversationId
New follower / follow request / request acceptedfollowActivity/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.

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.