Enables your users to receive push notifications from your network.
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:
| Channel | Driver class | Target |
|---|---|---|
| Browser / PWA | drivers/Fcm.php | Any browser or PWA that registered via the Firebase JS SDK |
| Official HumHub mobile app | drivers/Proxy.php | The community HumHub app (iOS/Android), relayed via https://push.humhub.com |
| Branded mobile app (custom FCM) | drivers/Fcm.php | A custom-branded app that uses the operator's own Firebase project |
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
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.
ConfigureForm)All settings are stored via $module->settings (HumHub key-value store).
| Setting | Driver | Description |
|---|---|---|
humhubApiKey | Proxy | API key obtained from the HumHub push service portal |
senderId | Fcm | Firebase Messaging Sender ID (numeric) |
firebaseApiKey | Fcm | Firebase Web API Key |
firebaseAppId | Fcm | Firebase Web App ID |
firebaseVapidKey | Fcm | VAPID key pair from Firebase Web Push Certificates |
json | Fcm | Full 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.
FcmUser modelDatabase table: fcmpush_user
| Column | Description |
|---|---|
id | PK |
user_id | HumHub user |
token | FCM registration token (unique, issued by the browser/app) |
sender_id | Identifies which Firebase project/driver owns this token |
created_at / updated_at | Timestamps 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.
Native apps (Flutter):
Events::onAfterLogin() sets SESSION_VAR_REGISTER_NOTIFICATION in the session.Events::onLayoutAddonInit() detects the flag and calls MobileAppHelper::registerNotificationScript(), which sends a JS bridge message {type: 'registerFcmDevice', url: '…'} to the Flutter app./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:
web/pwa module calls the global afterServiceWorkerRegistration(registration) whenever the service worker registers or updates.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()):
updated_at is refreshed.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.
/fcm-push/admin/debug) lists all tokens for the currently logged-in admin and provides a delete link.notification module iterates MobileTargetProvider implementations.onBeforeRequest replaced MobileTargetProvider::class with NotificationTargetProvider::class in the DI container, NotificationTargetProvider::handle() is called.handle() sets the locale to the target user's, then delegates to MessagingService::processNotification().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().TokenService::getTokensForUser() fetches all tokens scoped to that driver's sender_id. If tokens exist, driver->processCloudMessage() is called.Fcm driverUses 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 driverMakes 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.
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 | Handler | Purpose |
|---|---|---|
Application::EVENT_BEFORE_REQUEST | onBeforeRequest | Registers NotificationTargetProvider in the DI container when a driver is configured |
ServiceWorkerController::EVENT_INIT | onServiceWorkerControllerInit | Injects Firebase SDK bootstrap into the PWA service worker |
LayoutAddons::EVENT_INIT | onLayoutAddonInit | Triggers token registration (after login) or unregistration (after logout) scripts |
User::EVENT_AFTER_LOGIN | onAfterLogin | Sets the session flag to trigger token registration on next render |
User::EVENT_AFTER_LOGOUT | onAfterLogout | Sets session flags to trigger token unregistration on next render |
| Route | Controller action | Description |
|---|---|---|
POST /fcm-push/token/update | TokenController::actionUpdate | Register/refresh a browser FCM token |
POST /fcm-push/token/update-mobile-app | TokenController::actionUpdateMobileApp | Register/refresh a mobile app FCM token |
POST /fcm-push/token/delete | TokenController::actionDelete | Remove a browser FCM token |
POST /fcm-push/token/delete-mobile-app | TokenController::actionDeleteMobileApp | Remove a mobile app FCM token |
GET /fcm-push/status | StatusController::actionIndex | Returns HTTP 200 / 404 / 501 JSON to check module health |
GET /fcm-push/admin | AdminController::actionIndex | Admin configuration page |
GET /fcm-push/admin/debug | AdminController::actionDebug | Lists 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.
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.
| Class | Role |
|---|---|
Module | Module entry point; lazy-loads ConfigureForm and DriverService |
ConfigureForm | Loads/saves all settings; validates the service account JSON |
DriverService | Instantiates configured drivers; routes to web vs. mobile driver |
DriverInterface | Contract every driver must implement |
drivers/Fcm | Direct Firebase delivery via kreait/firebase-php SDK |
drivers/Proxy | Delivery via the HumHub push relay service |
TokenService | CRUD for FcmUser records (device tokens) |
MessagingService | Orchestrates notification → driver dispatch |
NotificationTargetProvider | Bridge between HumHub's notification system and this module |
FcmUser | ActiveRecord for the fcmpush_user table |
MobileAppHelper | Emits Flutter JS bridge messages to register/unregister devices |
WebAppHelper | Emits JS to unregister web browser tokens on logout |