Back to marketplace
59

Push Notifications (Firebase)

Enables your users to receive push notifications from your network.

FCM Push — Developer Guide

Overview

The fcm-push module adds Firebase Cloud Messaging (FCM) push notifications to HumHub. When a user receives a HumHub notification, this module dispatches it as a push message to all registered devices of that user.

It supports two delivery channels simultaneously:

ChannelDriver classTarget
Browser / PWAdrivers/Fcm.phpAny browser or PWA that registered via the Firebase JS SDK
Official HumHub mobile appdrivers/Proxy.phpThe community HumHub app (iOS/Android), relayed via https://push.humhub.com
Branded mobile app (custom FCM)drivers/Fcm.phpA custom-branded app that uses the operator's own Firebase project

Architecture

HumHub Notification System
        │
        ▼
NotificationTargetProvider      (implements MobileTargetProvider, replaces it via DI)
        │
        ▼
MessagingService::processNotification()
        │
        ├──► Fcm driver ──────► Firebase API (kreait/firebase-php SDK)
        │       └─ uses service account JSON + Sender ID
        │
        └──► Proxy driver ────► https://push.humhub.com/api/v1/push
                └─ uses humhubApiKey

Driver selection

DriverService initialises both drivers at boot and exposes two accessor methods:

  • getWebDriver() — returns the Fcm driver if configured; never the Proxy (the Proxy is only for native apps).
  • getMobileAppDriver() — returns Fcm when the request comes from a branded app (DeviceDetectorHelper::isAppWithCustomFcm()), otherwise returns Proxy.

Both drivers can be active at the same time. MessagingService::processMessage() iterates all configured drivers and dispatches to each independently.


Configuration (ConfigureForm)

All settings are stored via $module->settings (HumHub key-value store).

SettingDriverDescription
humhubApiKeyProxyAPI key obtained from the HumHub push service portal
senderIdFcmFirebase Messaging Sender ID (numeric)
firebaseApiKeyFcmFirebase Web API Key
firebaseAppIdFcmFirebase Web App ID
firebaseVapidKeyFcmVAPID key pair from Firebase Web Push Certificates
jsonFcmFull contents of the Firebase Service Account JSON file

ConfigureForm::getJsonAsArray() / getJsonParam() parse the raw JSON on the fly. The json field is validated against the expected Google service account structure on save.


Registered Devices — FcmUser model

Database table: fcmpush_user

ColumnDescription
idPK
user_idHumHub user
tokenFCM registration token (unique, issued by the browser/app)
sender_idIdentifies which Firebase project/driver owns this token
created_at / updated_atTimestamps set in beforeSave

One row = one registered device. A user can have many tokens (multiple browsers, multiple devices). The sender_id column lets Proxy tokens and Fcm tokens coexist for the same user without collision.

Token lifecycle

Created / updated

Native apps (Flutter):

  1. User logs in → Events::onAfterLogin() sets SESSION_VAR_REGISTER_NOTIFICATION in the session.
  2. On the next full page render → Events::onLayoutAddonInit() detects the flag and calls MobileAppHelper::registerNotificationScript(), which sends a JS bridge message {type: 'registerFcmDevice', url: '…'} to the Flutter app.
  3. The app obtains an FCM token from Firebase and POSTs it to /fcm-push/token/update-mobile-app.

Browsers / PWA (humhub.firebase.js):

Token registration is always routed through afterServiceWorkerRegistration(), which is the single code path for calling getToken. It is triggered in two ways:

  • PWA service worker callback — the web/pwa module calls the global afterServiceWorkerRegistration(registration) whenever the service worker registers or updates.
  • Proactive check in init() — on every page load, init() checks whether Notification.permission === 'granted' AND no token is cached in localStorage. If so, it calls afterServiceWorkerRegistration() via navigator.serviceWorker.ready. This covers users who granted permission in browser settings after their initial login, without requiring a logout/login cycle.

afterServiceWorkerRegistration() itself is protected by two guards to prevent double execution — which would cause two different tokens if both callers fire on the same page load (e.g. when a service worker update changes the registration object):

  • _tokenRegistrationPending flag — set synchronously at entry, reset in every .then()/.catch() branch. Whichever caller arrives second while the first is still awaiting its promise returns immediately.
  • getTokenLocalStore() check — if a valid token is already cached in localStorage (from an earlier call that already succeeded), skip silently.

Once getToken returns a token, sendTokenToServer() POSTs it to /fcm-push/token/update and caches it in localStorage with a 24-hour expiry. On subsequent page loads within that window isTokenSentToServer() returns true and no AJAX call is made.

Server-side (TokenService::storeTokenForUser()):

  • Token already exists for the same user + sender → updated_at is refreshed.
  • Token exists but belongs to a different user or sender → old record deleted, new one created (handles device hand-offs).

Deleted

  • On logout: Events::onAfterLogout() sets two distinct session flags — WebAppHelper::SESSION_VAR_UNREGISTER_NOTIFICATION ('webAppUnregisterNotification') and MobileAppHelper::SESSION_VAR_UNREGISTER_NOTIFICATION ('mobileAppUnregisterNotification'). On the next page render onLayoutAddonInit consumes each flag independently: the web block injects JS calling humhub.modules.firebase.unregisterNotification(), and the mobile block sends an {type: 'unregisterFcmDevice'} Flutter bridge message. Both ultimately POST to TokenController::actionDelete[MobileApp]()TokenService::deleteToken().

    ⚠️ The two constants must have different string values. If they share a value the first block removes the session key and the mobile unregister script never fires, leaving stale tokens in the database.

    ℹ️ unregisterNotification() reads the token from localStorage and deletes that one token. Only the token cached in the current browser is removed on logout; tokens registered from other browsers or devices remain active in the database.

  • Auto-cleanup of permanently rejected tokens: After every multicast send, MessagingService::processMessage() inspects SendReport::$failedTokens. Only tokens that Firebase has permanently invalidated (unknownTokens = app uninstalled / invalidTokens = malformed) are deleted via TokenService::deleteToken(). Tokens that failed due to transient errors (rate limit, server hiccup) or because a device was simply offline are never deleted — Firebase queues messages for offline devices and does not report them as failures.

  • Manually by an admin: The debug page (/fcm-push/admin/debug) lists all tokens for the currently logged-in admin and provides a delete link.

Notification dispatch flow

  1. A HumHub notification is triggered anywhere in the application.
  2. The core notification module iterates MobileTargetProvider implementations.
  3. Because onBeforeRequest replaced MobileTargetProvider::class with NotificationTargetProvider::class in the DI container, NotificationTargetProvider::handle() is called.
  4. handle() sets the locale to the target user's, then delegates to MessagingService::processNotification().
  5. MessagingService builds the message (title = site name, body = notification text, url = notification entry URL, icon = site icon at 180 px, badge count = unseen notifications count) and calls processMessage().
  6. For each configured driver, TokenService::getTokensForUser() fetches all tokens scoped to that driver's sender_id. If tokens exist, driver->processCloudMessage() is called.

Fcm driver

Uses the kreait/firebase-php SDK (loaded via a dedicated vendor/autoload.php inside the module). Authenticates with the service account JSON. Sends a multicast message with withWebPushConfig (link) and withData (url + notification_count). The imageUrl is intentionally omitted from the notification payload to avoid displaying a duplicate of the logo on branded apps.

After each multicast, the driver inspects the MulticastSendReport for permanently invalid tokens only:

  • unknownTokens() — Firebase returned UNREGISTERED (app was uninstalled, token expired). Safe to delete.
  • invalidTokens() — token is structurally malformed. Safe to delete.

Transient failures (rate limit, server unavailable) appear in failures() but not in the two lists above and are not deleted. A device being off never generates a Firebase failure at all — Firebase queues the message and delivers it when the device reconnects — so offline devices are never at risk of having their token removed.

Proxy driver

Makes an authenticated HTTP POST to https://push.humhub.com/api/v1/push using the humhubApiKey as a Bearer token. The HumHub service relays the message to FCM on behalf of the operator.


Service Worker integration (PWA)

Events::onServiceWorkerControllerInit() hooks into the web/pwa module's service worker generation. If the Fcm driver is configured, it appends importScripts calls for the Firebase compat SDK and the firebase.initializeApp() + firebase.messaging() bootstrap to the service worker JS. This is required for background message reception in PWA mode.


Event hooks summary

EventHandlerPurpose
Application::EVENT_BEFORE_REQUESTonBeforeRequestRegisters NotificationTargetProvider in the DI container when a driver is configured
ServiceWorkerController::EVENT_INITonServiceWorkerControllerInitInjects Firebase SDK bootstrap into the PWA service worker
LayoutAddons::EVENT_INITonLayoutAddonInitTriggers token registration (after login) or unregistration (after logout) scripts
User::EVENT_AFTER_LOGINonAfterLoginSets the session flag to trigger token registration on next render
User::EVENT_AFTER_LOGOUTonAfterLogoutSets session flags to trigger token unregistration on next render

HTTP endpoints

RouteController actionDescription
POST /fcm-push/token/updateTokenController::actionUpdateRegister/refresh a browser FCM token
POST /fcm-push/token/update-mobile-appTokenController::actionUpdateMobileAppRegister/refresh a mobile app FCM token
POST /fcm-push/token/deleteTokenController::actionDeleteRemove a browser FCM token
POST /fcm-push/token/delete-mobile-appTokenController::actionDeleteMobileAppRemove a mobile app FCM token
GET /fcm-push/statusStatusController::actionIndexReturns HTTP 200 / 404 / 501 JSON to check module health
GET /fcm-push/adminAdminController::actionIndexAdmin configuration page
GET /fcm-push/admin/debugAdminController::actionDebugLists and allows deletion of tokens for the current user

CSRF validation is disabled on TokenController because calls originate from the Firebase JS SDK or the Flutter app, which do not carry a CSRF token.


Console command (testing)

php yii firebase/send-to-user <userId> "<title>" "<message>"

Sends a raw push message to all registered devices for <userId> via all configured drivers. Useful for verifying that configuration is correct without triggering a real notification.


Key classes at a glance

ClassRole
ModuleModule entry point; lazy-loads ConfigureForm and DriverService
ConfigureFormLoads/saves all settings; validates the service account JSON
DriverServiceInstantiates configured drivers; routes to web vs. mobile driver
DriverInterfaceContract every driver must implement
drivers/FcmDirect Firebase delivery via kreait/firebase-php SDK
drivers/ProxyDelivery via the HumHub push relay service
TokenServiceCRUD for FcmUser records (device tokens)
MessagingServiceOrchestrates notification → driver dispatch
NotificationTargetProviderBridge between HumHub's notification system and this module
FcmUserActiveRecord for the fcmpush_user table
MobileAppHelperEmits Flutter JS bridge messages to register/unregister devices
WebAppHelperEmits JS to unregister web browser tokens on logout

Module Information

Price:
Free
Latest version release:
2.2.4 - May 15, 2026
Website:
Compatibility:
HumHub 1.4 - 1.18