axiom-push-notifications-diag

Push Notification Diagnostics

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "axiom-push-notifications-diag" with this command: npx skills add charleswiltgen/axiom/charleswiltgen-axiom-axiom-push-notifications-diag

Push Notification Diagnostics

Systematic troubleshooting for push notification failures: missing notifications, token registration errors, environment mismatches, silent push throttling, and service extension problems.

Overview

Core Principle: When push notifications don't work, the problem is usually:

  • Token/registration failures (never registered, wrong format, expired) — 30%

  • Entitlement/provisioning mismatch (capability missing, wrong environment) — 25%

  • Payload structure errors (missing keys, wrong types, invalid JSON) — 15%

  • Focus/interruption suppression (iOS 15+ filtering, provisional auth) — 15%

  • Service extension failures (timeout, crash, missing mutable-content) — 10%

  • Delivery timing/throttling (silent push budget, APNs coalescing) — 5%

Always verify entitlements and token registration BEFORE debugging payload or delivery logic.

Red Flags

Symptoms that indicate push-specific issues:

Symptom Likely Cause

No notifications at all Missing Push Notification capability or provisioning profile

Works in dev, not production Sending to sandbox APNs with production token (or vice versa)

Token registration fails on Simulator Expected — Simulator cannot register for remote notifications

Notifications appear without sound Missing .sound in authorization options or payload

Rich notification shows plain text Missing mutable-content: 1 in payload

Image not showing in notification Service extension failed silently — check serviceExtensionTimeWillExpire

Silent push not waking app System throttling (~2-3/hour), or app was force-quit by user

Notifications stopped after iOS update Focus mode enabled by default in iOS 15+; check interruption level

Badge shows wrong number Multiple notifications sent without explicit badge count reset

Actions not appearing Category identifier mismatch between payload and registered categories

Notification appears twice Both local and remote notification scheduled for same event

FCM works on Android, not iOS Missing APNs auth key upload in Firebase Console

Anti-Rationalization

Rationalization Why It Fails Time Cost

"It worked yesterday, so entitlements are fine" Provisioning profiles get regenerated during signing changes. Always re-verify. 30-60 min debugging code when the profile lost push capability

"The server says their payload is fine" 55% of push failures are client-side (entitlements + tokens). Verify independently with curl. 1-2 hours of finger-pointing before someone checks

"I'll skip token verification, the error is clearly in the payload" Wrong-environment tokens are the #1 cause of "works in dev, not production." 30+ min debugging valid payloads sent to invalid tokens

"Focus mode doesn't matter, we use default interruption level" Default (active ) is filtered by Focus. Only time-sensitive and critical break through. Hours adding code workarounds for a payload-level fix

"Silent push is reliable, we use it for sync" System throttles to ~2-3/hour and ignores force-quit apps. It's a hint, not a guarantee. Architecture rework when silent push can't sustain real-time sync

"Service extension is set up, so rich notifications should work" Extension needs correct bundle ID suffix, mutable-content in payload, AND completing within 30s. 30+ min when any one of the three prerequisites is missing

"FCM handles everything, I don't need to understand APNs" FCM wraps APNs. Token type confusion, missing p8 key upload, and swizzling conflicts are all APNs-level problems. Hours debugging FCM when the issue is APNs configuration

"I'll test on Simulator first" Simulator cannot register for remote notifications. No APNs token = no real push testing. Wasted test cycle discovering Simulator limitations

"Let me rewrite the notification handler" 80% of push failures are configuration (entitlements, tokens, environment), not code. Hours rewriting working code while the config stays broken

"This worked on iOS 17, the bug must be in our code" Each iOS version changes Focus defaults, interruption filtering, and provisional behavior. Debugging code when the fix is a payload or Settings change

Mandatory First Steps

Before investigating code, run these diagnostics:

Step 1: Verify Push Notification Entitlements

security cms -D -i path/to/embedded.mobileprovision | grep -A1 "aps-environment"

Expected output:

  • ✅ <string>development</string> or <string>production</string> → Entitlement present

  • ❌ No aps-environment key → Push Notifications capability not enabled in Xcode

How to find the provisioning profile:

For installed app on device

find ~/Library/Developer/Xcode/DerivedData -name "embedded.mobileprovision" -newer . 2>/dev/null | head -3

Step 2: Check Token Registration

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%02x", $0) }.joined() print("✅ APNs token: (token)") print("✅ Token length: (token.count) chars") }

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { print("❌ Registration failed: (error.localizedDescription)") }

Expected output:

  • ✅ 64-character hex token → Registration successful

  • ❌ "no valid aps-environment entitlement" → Capability misconfigured

  • ❌ No callback fires at all → registerForRemoteNotifications() never called

Critical: Both callbacks must be in AppDelegate , not SceneDelegate . SwiftUI apps need @UIApplicationDelegateAdaptor .

Step 3: Validate Payload with curl

curl -v
--header "apns-topic: com.your.bundle.id"
--header "apns-push-type: alert"
--header "authorization: bearer $JWT_TOKEN"
--data '{"aps":{"alert":{"title":"Test","body":"Hello"}}}'
--http2 https://api.sandbox.push.apple.com/3/device/$DEVICE_TOKEN

Expected output:

  • ✅ HTTP/2 200 → Payload accepted by APNs

  • ❌ 400 BadDeviceToken → Token format wrong or expired

  • ❌ 403 ExpiredProviderToken → JWT older than 1 hour

  • ❌ 403 InvalidProviderToken → Wrong key ID, team ID, or key

  • ❌ 410 Unregistered → App uninstalled or token invalidated

  • ❌ 413 PayloadTooLarge → Exceeds 4096 bytes

Step 4: Check Authorization Status

let settings = await UNUserNotificationCenter.current().notificationSettings() print("Authorization: (settings.authorizationStatus.rawValue)") print("Alert: (settings.alertSetting.rawValue)") print("Sound: (settings.soundSetting.rawValue)") print("Badge: (settings.badgeSetting.rawValue)")

Expected output:

  • ✅ authorizationStatus = 2 → Authorized

  • ⚠️ authorizationStatus = 3 → Provisional (appears silently in Notification Center)

  • ❌ authorizationStatus = 1 → Denied by user

  • ❌ authorizationStatus = 0 → Not determined (never requested)

Decision Trees

Tree 1: Not Receiving Any Notifications

Not receiving any notifications? │ ├─ Check Step 1 (entitlements) │ ├─ No aps-environment key? │ │ └─ Enable Push Notifications in Signing & Capabilities → DONE │ └─ aps-environment present → continue │ ├─ Check Step 2 (token registration) │ ├─ didFailToRegister called? │ │ ├─ "no valid aps-environment" → Regenerate provisioning profile │ │ └─ Other error → Check network, device (not Simulator) │ ├─ Neither callback fires? │ │ └─ Verify registerForRemoteNotifications() called after app launch │ └─ Token received → continue │ ├─ Check Step 3 (payload delivery) │ ├─ HTTP 200 but no notification? │ │ └─ Check Step 4 (authorization status) │ ├─ 400 BadDeviceToken? │ │ └─ Token expired or wrong environment → Re-register │ └─ 403/410 error? │ └─ Fix auth credentials or re-register device │ └─ Check Step 4 (user authorization) ├─ Status: denied? │ └─ User must enable in Settings → Show settings prompt ├─ Status: notDetermined? │ └─ Call requestAuthorization() → Was never requested └─ Status: authorized but still no notifications? └─ Check Focus mode, Do Not Disturb, notification grouping

Tree 2: Works in Dev, Not Production

Works in development, fails in production? │ ├─ APNs endpoint correct? │ ├─ Dev: api.sandbox.push.apple.com │ └─ Prod: api.push.apple.com │ └─ Using sandbox endpoint with production build? → Switch endpoint │ ├─ Token environment matches? │ ├─ Dev and production tokens are DIFFERENT │ │ └─ Server storing dev token, sending to prod APNs? → Re-register on prod build │ └─ Server distinguishes token environments? → Add environment flag to token storage │ ├─ Auth method correct? │ ├─ .p8 key (token-based)? │ │ └─ Same key works for both environments ✅ │ └─ .p12 certificate? │ ├─ Dev cert → Only works with sandbox │ └─ Prod cert → Only works with production │ └─ Wrong cert for environment? → Generate correct certificate │ └─ Using FCM? ├─ APNs auth key (.p8) uploaded to Firebase Console? │ └─ Missing? → Upload in Project Settings > Cloud Messaging └─ Key uploaded but wrong Team ID? └─ Verify Team ID matches Apple Developer account

Tree 3: Silent Notifications Not Waking App

Silent push not waking app? │ ├─ Payload correct? │ ├─ Has "content-available": 1 in aps? │ │ └─ Missing? → Add to aps dictionary │ ├─ Has NO "alert", "badge", or "sound" in aps? │ │ └─ Has alert? → Not a silent push; system treats as visible notification │ └─ Payload valid → continue │ ├─ Headers correct? │ ├─ apns-push-type: background? │ │ └─ Missing or wrong? → Must be "background" for silent push │ └─ apns-priority: 5? │ └─ Using 10? → Silent push MUST use priority 5 │ ├─ Background mode enabled? │ ├─ "Remote notifications" checked in Background Modes capability? │ │ └─ Missing? → Enable in Signing & Capabilities │ └─ application(_:didReceiveRemoteNotification:fetchCompletionHandler:) implemented? │ └─ Missing? → Implement the delegate method │ ├─ App state? │ ├─ Force-quit by user (swiped up)? │ │ └─ System will NOT wake force-quit apps for silent push │ └─ Suspended or background? │ └─ Should wake — continue debugging │ └─ System throttling? ├─ Budget: ~2-3 silent pushes per hour │ └─ Exceeding? → Reduce frequency, batch updates └─ Device in Low Power Mode? └─ Further reduces background execution budget

Tree 4: Rich Notification Missing Media

Rich notification not showing image/video? │ ├─ Payload has mutable-content: 1? │ └─ Missing? → Required for Notification Service Extension to fire │ ├─ Notification Service Extension target exists? │ ├─ Missing? → File > New > Target > Notification Service Extension │ └─ Exists → continue │ ├─ Extension bundle ID correct? │ ├─ Must be: {app-bundle-id}.{extension-name} │ │ Example: com.myapp.NotificationService │ └─ Wrong prefix? → Fix bundle ID to match parent app │ ├─ Download completing in time? │ ├─ Extension has ~30 seconds to modify notification │ │ └─ Large file? → Use thumbnail URL, not full resolution │ └─ serviceExtensionTimeWillExpire called? │ └─ Must deliver bestAttemptContent with fallback text │ ├─ Attachment created correctly? │ ├─ File written to disk before creating UNNotificationAttachment? │ │ └─ Must write to tmp directory, then create attachment from file URL │ └─ File type supported? │ ├─ Images: JPEG, GIF, PNG (max 10MB) │ ├─ Audio: AIFF, WAV, MP3, M4A (max 5MB) │ └─ Video: MPEG, MPEG-2, MP4, AVI (max 50MB) │ └─ App groups configured? └─ Extension and app share data via App Groups? └─ Missing? → Add same App Group to both targets

Tree 5: Live Activity Not Updating via Push

Live Activity not updating from push? │ ├─ APNs topic correct? │ ├─ Must be: {bundleID}.push-type.liveactivity │ │ └─ Using plain bundle ID? → Append .push-type.liveactivity │ └─ Topic correct → continue │ ├─ Push type header correct? │ ├─ apns-push-type: liveactivity? │ │ └─ Using "alert"? → Must be "liveactivity" │ └─ Correct → continue │ ├─ Content-state matches ActivityAttributes.ContentState? │ ├─ JSON keys match Swift property names exactly? │ │ └─ Mismatch? → Decoding fails silently │ └─ Using custom CodingKeys or JSONEncoder strategies? │ └─ Custom strategies NOT supported — use default key encoding │ ├─ Push token being sent to server? │ ├─ Observing pushTokenUpdates on the Activity? │ │ └─ Missing? → Must iterate Activity.pushTokenUpdates async sequence │ └─ Token changes when Activity restarts? │ └─ Must handle token rotation — send updated token to server │ └─ Rate limiting? ├─ Frequent updates: ~10-12 per hour per Activity │ └─ Exceeding? → Batch updates, reduce frequency └─ Alert updates (sound/vibration): ~3-4 per hour └─ Exceeding? → Reserve alerts for critical state changes

Tree 6: Notifications Stopped After iOS Update

Notifications stopped working after iOS update? │ ├─ Focus mode auto-enabled? (iOS 15+) │ ├─ Check Settings > Focus │ │ └─ Focus active? → App may not be in allowed list │ └─ No Focus active → continue │ ├─ Interruption level filtering? │ ├─ Default level is .active (may be filtered by Focus) │ │ └─ Need to break through Focus? → Use .timeSensitive or .critical │ ├─ .timeSensitive requires capability │ │ └─ Missing? → Add Time Sensitive Notifications capability │ └─ .critical requires Apple entitlement │ └─ Only for health/safety/security apps — apply via Apple Developer │ ├─ Provisional authorization behavior changed? │ ├─ iOS 15+ provisional notifications appear in Notification Summary │ │ └─ User may not see them → Request full authorization │ └─ Was relying on provisional? → Prompt for explicit permission │ └─ Communication notifications require INSendMessageIntent? ├─ iOS 15+ communication notifications need SiriKit intent │ └─ Missing? → Donate INSendMessageIntent before showing notification └─ Intent donated but still filtered? └─ Check that sender is in user's contacts

Push Notification Console Workflow

Apple's Push Notification Console provides server-free testing:

Navigate to Console

Send Test Notification

  • Enter device token (from Step 2 diagnostic)

  • Select environment (Sandbox/Production)

  • Compose payload or use template

  • Send and observe delivery status

Check Delivery Logs

  • Copy apns-id from the response header of your push request

  • Use Push Notification Console to look up delivery status by apns-id

  • Status shows: accepted, delivered, dropped (with reason)

Common Console Findings

Status Meaning Action

Delivered APNs delivered to device Problem is on-device (auth, Focus, extension)

Dropped: Unregistered Token invalid Re-register device

Dropped: DeviceTokenNotForTopic Bundle ID mismatch Fix apns-topic header

Stored Device offline, will deliver later Wait or check device connectivity

Simulator Testing with simctl

Simulators cannot register for remote notifications, but you can test notification handling:

cat > test-push.apns << 'EOF' { "Simulator Target Bundle": "com.your.bundle.id", "aps": { "alert": { "title": "Test", "body": "Hello from simctl" }, "sound": "default", "mutable-content": 1 } } EOF

xcrun simctl push booted com.your.bundle.id test-push.apns

Simulator Limitations

  • ✅ Notification appearance and content

  • ✅ Notification Service Extension processing

  • ✅ Notification Content Extension (custom UI)

  • ✅ Action handling and categories

  • ❌ APNs token registration (always fails)

  • ❌ Silent push waking app accurately

  • ❌ Live Activity push updates

Drag-and-Drop Alternative

Drag a .apns file directly onto the Simulator window to deliver it. Requires "Simulator Target Bundle" key in the payload.

Common FCM Diagnostics

Swizzling Conflict

Symptom: Token callback not firing with Firebase

Cause: Method swizzling disabled but manual forwarding not implemented

Diagnostic:

// Check Info.plist // FirebaseAppDelegateProxyEnabled = NO means YOU must forward tokens

Fix (if swizzling disabled):

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken }

Token Mismatch

Symptom: Server has FCM token, but APNs delivery fails

Cause: FCM token and APNs token are different. FCM wraps APNs token.

Diagnostic:

// FCM token (send this to YOUR server) Messaging.messaging().token { token, error in print("FCM token: (token ?? "nil")") }

// APNs token (FCM handles this internally) // Do NOT send raw APNs token to your server when using FCM

Missing APNs Key in Firebase Console

Symptom: FCM works on Android, notifications not arriving on iOS

Fix:

  • Firebase Console → Project Settings → Cloud Messaging

  • Upload APNs Authentication Key (.p8)

  • Enter Key ID and Team ID

  • Verify bundle ID matches your app

Quick Reference Table

Symptom Check Fix

No notifications at all Step 1: entitlements Enable Push Notification capability, regenerate profile

Token registration fails Step 2: callbacks Implement both delegate methods in AppDelegate

400 BadDeviceToken Token format Re-register; check hex encoding (no spaces, no angle brackets)

403 InvalidProviderToken JWT/certificate Regenerate JWT; verify key ID, team ID, bundle ID

410 Unregistered Device state App uninstalled or token rotated — remove from server

Works dev not prod Step 3: curl both Switch APNs endpoint; tokens differ per environment

Silent push ignored Payload + headers content-available: 1, push-type: background, priority: 5

Rich media missing Extension Add mutable-content: 1, check extension bundle ID and timeout

Live Activity stale Topic format Use {bundleID}.push-type.liveactivity topic

Focus mode filtering Interruption level Use .timeSensitive for important notifications

FCM iOS failure Firebase Console Upload .p8 key with correct Key ID and Team ID

Actions not showing Category ID Match category identifier in payload to registered categories

Pressure Scenarios

Scenario 1: "Server team says the problem is on the iOS side"

Context: Push notifications stopped working. The backend team says their payload is fine and the problem must be in the app.

Pressure: Skip client-side diagnostics and assume the server is right. Start rewriting notification handling code.

Reality: 55% of push failures are entitlement/token issues (Steps 1-2), not code bugs. The server may be sending to the wrong environment or using an expired token.

Correct action: Run all 4 mandatory diagnostic steps before touching code. Share the curl test (Step 3) results with the server team — this objectively proves which side has the issue.

Push-back template: "Let me verify the client-side chain first — I can share the curl results in 5 minutes so we both know exactly where the failure is."

Scenario 2: "Notifications stopped after iOS update, ship a fix today"

Context: Users report notifications stopped working after updating to a new iOS version. Management wants a hotfix today.

Pressure: Start debugging notification code immediately. Assume Apple broke something.

Reality: New iOS versions often enable Focus mode by default or change interruption level filtering. 15% of push failures are Focus/interruption suppression — no code change needed on your side.

Correct action: Check Tree 6 ("Notifications stopped after iOS update"). Verify Focus mode settings on test devices before changing any code. If Focus is filtering, the fix is setting the correct interruption-level in the payload, not rewriting notification handling.

Push-back template: "iOS updates often change Focus mode defaults. Let me check interruption levels first — if that's the cause, the fix is a one-line payload change, not a code rewrite."

Scenario 3: "Silent push worked last week, nothing changed"

Context: Background content sync via silent push stopped working. "We didn't change anything."

Pressure: Deep-dive into background execution code. Assume a regression.

Reality: Silent push has a system-enforced throttle budget (~2-3/hour). If usage increased, or if users force-quit the app, silent push stops working regardless of code quality. Also, the provisioning profile may have been regenerated without the push entitlement.

Correct action: Follow Tree 3 ("Silent notifications not waking app"). Check throttle budget, force-quit state, and entitlements before debugging code.

Push-back template: "Silent push has a system throttle budget. Let me verify we haven't exceeded it and that the app hasn't been force-quit on test devices — those are the two most common causes."

Checklist

Before escalating push notification issues:

  • Push Notification capability enabled in Xcode (Step 1)

  • Provisioning profile contains aps-environment (Step 1)

  • Token registration callback fires with 64-char hex token (Step 2)

  • curl to APNs returns HTTP/2 200 (Step 3)

  • User authorized notifications, status = 2 (Step 4)

  • APNs environment matches build type (sandbox/production)

  • Focus mode not filtering notifications on test device

  • Tested on physical device (not Simulator for token registration)

  • For FCM: APNs auth key uploaded to Firebase Console

  • For silent push: background mode enabled, priority 5, no alert keys

Resources

WWDC: 2021-10091, 2023-10025, 2023-10185

Docs: /usernotifications, /usernotifications/testing-notifications-using-the-push-notification-console

Skills: axiom-push-notifications, axiom-push-notifications-ref, axiom-extensions-widgets

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

axiom-vision

No summary provided by upstream source.

Repository SourceNeeds Review
General

axiom-swiftdata

No summary provided by upstream source.

Repository SourceNeeds Review
General

axiom-swiftui-26-ref

No summary provided by upstream source.

Repository SourceNeeds Review
General

axiom-swiftui-architecture

No summary provided by upstream source.

Repository SourceNeeds Review