Summary
mam-main uses FCM v1 (the OAuth2 service-account JWT path), not the deprecated server-key API. Implementation is in includes/push-notification-manager/android/.
Components
| Class | Path | Role |
|---|---|---|
MAM_AndroidPushSender |
android/ |
FCM v1 sender |
MAM_AndroidPayloadBuilder |
android/ |
Builds the FCM JSON payload |
MAM_AndroidCredentials |
android/ |
Loads the Firebase service-account JSON path + project id |
MAM_FirebaseJwtSigner |
auth/ |
Signs the OAuth2 JWT for FCM v1 |
Device tokens are read directly via get_user_meta() in the mam_push_notification_manager bridge — the repo/MAM_PushTokenRepository.php file is an empty stub, not an implemented class.
Credentials
| Asset | WordPress option |
|---|---|
| Firebase service-account JSON | android_pn_pem_certs — the filename; MAM_AndroidCredentials resolves it to mam-main/pemcerts/<file> |
| FCM project id | mam_firebase_project_id (a distinct option — not read from the JSON) |
MAM_AndroidCredentials::load() returns null (and emits an admin_alert) if either the service-account file or the project id is missing. The service-account must have the messaging.send scope, or FCM v1 returns 403 Forbidden and the send silently fails.
MAM_AndroidCredentials is the only legitimate read path.
⚠️ Plaintext storage. Same hardening gap as APNs.
OAuth2 JWT flow
MAM_FirebaseJwtSigner signs a JWT using the service-account’s private key:
- Header:
{ "alg": "RS256", "typ": "JWT", "kid": "<service-account-key-id>" } - Claims:
{ "iss": "<service-account-email>", "scope": "https://www.googleapis.com/auth/firebase.messaging", "aud": "https://oauth2.googleapis.com/token", "iat": <now>, "exp": <now+1hr> } - Signed with the service-account’s RSA private key
The JWT is exchanged at oauth2.googleapis.com/token for an OAuth2 access token. The access token is cached for ~1 hour. Subsequent FCM sends use the access token in the Authorization: Bearer header.
Send endpoint
POST https://fcm.googleapis.com/v1/projects/<project_id>/messages:send
Authorization: Bearer <oauth2_access_token>
Content-Type: application/json
{
"message": {
"token": "<device_fcm_token>",
"notification": { "title": "...", "body": "..." },
"data": { ... },
"android": { "priority": "high", ... }
}
}
Device tokens
usermeta key mam_app_android_pn_token (written by the app on first launch).
Common errors
| FCM error | Meaning | Action |
|---|---|---|
UNREGISTERED |
Token is invalid (app uninstalled, etc.) | Drop the stored token |
INVALID_ARGUMENT |
Bad payload shape | Fix the payload builder |
MISMATCH_SENDER_ID |
Token belongs to a different FCM project | Check the service-account JSON; token might be from a previous app build |
QUOTA_EXCEEDED |
Per-project send quota exceeded | Back off |
UNAVAILABLE |
FCM transient outage | Retry with backoff (no automatic retry today) |
Send flow
do_action('mam_notification_send_pn', $msg)
│
▼
mam_push_notification_manager::send_push_notification()
│ wraps in MAM_PushNotification
▼
MAM_PushDispatcher::dispatch()
│
▼
MAM_AndroidPushSender::send()
│
▼
MAM_FirebaseJwtSigner::getAccessToken() ← cached for ~1hr
│
▼
HTTP POST to FCM v1 endpoint
│
▼
MAM_PushResult (status + errors per-token)
Gotchas
- FCM v1, not legacy. Don’t try to use a server key; mam-main only supports v1.
messaging.sendscope is mandatory. A service-account without it returns403 Forbidden.- Token churn — FCM tokens change when the user reinstalls or clears app data.
UNREGISTEREDerrors should drop the stored token. MISMATCH_SENDER_IDtypically means the device installed a previous app build under a different FCM project. The customer needs to reinstall.- No retry on transient
UNAVAILABLE— failed sends stay failed. - Plaintext credentials are a known hardening gap.
Related articles
- Recipe: Configure push notifications
- Integration: Apple Push Notification service (APNs)
- Notification channels: email, SMS, push
- Hook: mam_notification_send_pn
Metadata
| Field | Value |
|---|---|
| Article type | Plugin Overview |
| Plugin slug | mam-main |
| Applies to plugin version | 2.1.11+ |
| Category | Plugin Reference |
| Audience | PHP developer |
| Last verified | 2026-05-02 |
