Summary
mam-main talks to APNs via HTTP/2 using token-based authentication (a .p8 key, not a .p12 cert) for standard pushes. Voice-over-IP pushes use a separate cert and a separate sender because Apple treats VoIP as a distinct push topic with stricter rules.
Implementation lives in includes/push-notification-manager/.
Components
| Class | Path | Role |
|---|---|---|
MAM_IosPushSender |
ios/ |
Standard APNs sender (production → sandbox fallback) via the bundled Pushok library |
MAM_IosVoipPushSender |
ios/ |
VoIP APNs sender (separate token, production → sandbox fallback) |
MAM_IosPayloadBuilder |
ios/ |
Builds the APNs payload (returns a PushokPayload) |
MAM_IosCredentials |
ios/ |
Loads key id + team id + bundle id + auth-key file path from options |
JWT provider-token signing and the HTTP/2 transport are handled by the bundled Pushok library (PushokAuthProvider, PushokClient), not by a mam-main class. The auth/MAM_ApnsTokenProvider.php and repo/MAM_PushTokenRepository.php files are empty stubs (planned extraction, not yet implemented); device tokens are read directly via get_user_meta() in the mam_push_notification_manager bridge.
The senders implement MAM_PushSenderInterface (contracts/):
interface MAM_PushSenderInterface {
public function send( MAM_PushNotification $notification ): MAM_PushResult;
}
Credential storage
| Asset | WordPress option |
|---|---|
| APNs auth-key file | ios_pn_pem_certs — the filename; MAM_IosCredentials resolves it to mam-main/pemcerts/<file> |
| APNs key id | ios_pn_key_id |
| APNs team id | ios_pn_team_id |
| iOS bundle id | ios_pn_app_bundle_id (frozen — predates mam_* rename) |
⚠️ Plaintext storage. Encryption-at-rest is a tracked hardening task. Don’t expose credential options through any new UI without first addressing this.
MAM_IosCredentials is the only legitimate read path for these options — never read them directly elsewhere.
JWT provider tokens
APNs uses short-lived JWT provider tokens signed with the .p8 auth key. In mam-main this is handled by the bundled Pushok library’s AuthProviderToken, not a mam-main class:
MAM_IosCredentialssupplies the auth-key file path, key id, team id, and bundle idPushokAuthProviderToken::create()reads the key and signs the JWT (kid: <key_id>,iss: <team_id>,iat: <now>)- Pushok caches and reuses the provider token across the notifications in a send
PushokClientsends over HTTP/2
Apple rejects stale tokens with ExpiredProviderToken; Pushok re-signs as needed. (The MAM_ApnsTokenProvider class named in earlier drafts is an unimplemented stub — see the components note above.)
Device tokens
Stored in usermeta:
| Key | Purpose |
|---|---|
mam_app_ios_pn_token |
Standard APNs token |
mam_ios_voip_token |
VoIP APNs token (separate token) |
push_enabled |
User-side opt-in flag |
push_status |
Last delivery status |
push_status_updated |
Timestamp (update_user_meta( …, 'push_status_updated', time() )) |
Tokens are written by the mobile app on first launch via the user-roles AJAX endpoints. They’re read at send time via get_user_meta() in the mam_push_notification_manager bridge (there is no MAM_PushTokenRepository — that file is an empty stub).
⚠️ APNs vs APNs VoIP require separate tokens. A standard APNs token will NOT receive VoIP pushes and vice versa.
Payload structure
MAM_IosPayloadBuilder produces:
{
"aps": {
"alert": { "title": "...", "body": "..." },
"sound": "default",
"badge": 3,
"content-available": 1
},
"data": { ...arbitrary, deeplink etc. }
}
For VoIP, additional required fields enforced by MAM_IosVoipPushSender:
{
"aps": {
"alert": { ... },
"sound": "ringtone",
"interruption-level": "time-sensitive"
},
"callkit_payload": { ... }
}
⚠️ VoIP has a strict iOS deadline — the app must call CallKit within ~5 seconds of receiving the push or iOS kills the process and bans further VoIP pushes for the bundle.
Send flow
do_action('mam_notification_send_pn', $msg)
│
▼
mam_push_notification_manager::send_push_notification() ← legacy bridge
│ wraps in MAM_PushNotification
▼
MAM_PushDispatcher::dispatch( $notification )
│
┌────┴────────────────────┐
▼ ▼
$pn->type === 'voip' standard
& $pn->hasIos()? │
YES │
▼ ▼
MAM_IosVoipPushSender MAM_IosPushSender
│ │
▼ ▼
APNs HTTP/2 (VoIP APNs HTTP/2 (standard
token, prod→sandbox) .p8 + JWT, prod→sandbox)
│ │
▼ ▼
MAM_PushResult (per-token status + errors)
Common errors (from MAM_PushResult->errors)
| APNs error | Meaning | Action |
|---|---|---|
BadDeviceToken |
Token isn’t valid for this app/environment | Drop the stored token; the device will re-register |
Unregistered |
Token was valid but the app is uninstalled | Drop the token |
ExpiredProviderToken |
JWT is too old | Provider re-signs and retries automatically |
BadCertificateEnvironment |
Production cert sent to sandbox or vice versa | Check local-app-ios-app-channel |
PayloadTooLarge |
Payload >4KB | Reduce body length, move data to data block |
TooManyRequests |
Rate limit | Back off and retry |
Tokens flagged with BadDeviceToken / Unregistered should be dropped from usermeta to avoid wasting future sends — tracked as a hardening item; not currently automatic.
Gotchas
- APNs vs VoIP require separate tokens (VoIP is a distinct push topic).
- JWT staleness is handled automatically by the Pushok library; don’t try to cache tokens yourself elsewhere.
- Connection pooling not implemented.
MAM_IosPushSenderdoesn’t currently pool HTTP/2 connections across requests. High-volume sends pay TCP setup per request. Tracked. - VoIP 5-second deadline. Payload builder enforces required fields, but the app team must implement CallKit handoff correctly.
- Plaintext credentials are a known launch-readiness gap.
- Sandbox vs production — pushing to a TestFlight build with a production cert (or vice versa) silently fails with
BadCertificateEnvironment.
Related articles
- Recipe: Configure push notifications
- Integration: Firebase Cloud Messaging (FCM)
- 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 |
