Mobile Push Notifications skill for Codex

Pushes that arrive, tokens that stay clean. Codex picks the payload type each OS will actually deliver (alert, background, data, mutable content), sends through APNs token auth or the FCM v1 API, and stores tokens with their environment so debug tokens never hit production. It prunes tokens on Unregistered errors, builds Android channels and the Android 13 permission flow, and times the iOS prompt behind an in-app explanation.

  • Mobile & Desktop
  • $mobile-push-notifications
  • 168 lines
  • 2 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- mobile-push-notifications

Installs to ~/.agents/skills/mobile-push-notifications/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $mobile-push-notifications, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Implements push notifications for iOS and Android end to end - APNs token auth and headers, FCM HTTP v1 sends, device token lifecycle, alert versus background versus data payloads, Android notification channels and the POST_NOTIFICATIONS permission, iOS provisional authorization, notification service extensions, and permission prompts timed for opt-in. Use when notifications do not arrive, when wiring APNs or Firebase Cloud Messaging, designing payloads, migrating off the legacy FCM API, or improving push opt-in rates.

Use it when

  • Wiring APNs, Firebase Cloud Messaging or Expo notifications
  • Notifications arrive on one platform, build type or app state but not another
  • Designing payloads for alerts, silent refresh or rich media
  • Opt-in rate is low or the permission prompt fires at launch

Use something else when

  • You are updating a Live Activity or widget by push: use ios-widgets
  • The question is where a notification tap should navigate: use mobile-deep-links

How to check the result

Send to one device per platform in both debug and release environments across foreground, background and terminated states, then uninstall and confirm the backend deletes the token on the error response.

The mistake it prevents

Storing tokens without their APNs environment, so sandbox tokens from debug builds are sent to production and fail with BadDeviceToken.

~/.agents/skills/mobile-push-notifications/SKILL.md

--- name: mobile-push-notifications description: Implements push notifications for iOS and Android end to end - APNs token auth and headers, FCM HTTP v1 sends, device token lifecycle, alert versus… license: MIT ---

Done well means: every token the backend holds belongs to a live install and the right environment, each push uses the payload type the OS will actually deliver, users are asked for permission at a moment that explains the value, and failed sends prune dead tokens instead of retrying forever.

Before changing anything

  1. Read AGENTS.md, then find the existing stack: native APNs, Firebase Messaging on both platforms, Expo (expo-notifications), OneSignal or another provider. Do not mix two providers for the same platform.
  2. Locate token registration on the client and token storage on the server (table, TTL, user and device keys).
  3. Check entitlements and manifests: iOS aps-environment entitlement and Push capability; Android POST_NOTIFICATIONS in the manifest and a FirebaseMessagingService subclass.
  4. Real sends need network and credentials (.p8 key, service account JSON). Never paste keys into code or chat; read them from the environment or a secret manager.
  5. Sending a test push to real users is outward-facing: confirm the target token or topic with the user first, and default to a single test device.

Pick the payload type

GoaliOS (APNs)Android (FCM)
Visible alertapns-push-type: alert, aps.alertnotification block, or data and build it yourself
Silent refreshapns-push-type: background, aps.content-available: 1, priority 5, no alertdata only, normal priority
Modify before display (images, decryption)aps.mutable-content: 1 + Notification Service Extensiondata message, build in onMessageReceived
Live Activity updateapns-push-type: liveactivity (see ios-widgets)n/a
CallsPushKit VoIP, must report to CallKit immediatelyHigh-priority data + full-screen intent

Background pushes are best-effort on both platforms: throttled, dropped under low power, and never delivered on iOS after the user force-quits the app. Never rely on them for correctness; sync on next launch as well.

On Android, an FCM message with a notification block is shown by the system when the app is in the background and onMessageReceived is not called; with the app in the foreground, it is. If the app must always control display, send data only.

APNs essentials

  • Prefer token-based auth: one .p8 key per team, JWT signed ES256 with kid (key ID) and iss (team ID). Reuse the JWT for up to an hour and refresh it no more than every 20 minutes; regenerating per request triggers TooManyProviderTokenUpdates.
  • Endpoint https://api.push.apple.com/3/device/<token> over HTTP/2; development builds use api.sandbox.push.apple.com. A token from a debug build sent to production returns 400 BadDeviceToken; store the environment with every token.
  • Required headers: apns-topic (bundle ID), apns-push-type. Use apns-priority: 10 for alerts, 5 for background. Set apns-expiration and apns-collapse-id (max 64 bytes) for replaceable content such as scores.
  • Payload limit 4 KB (5 KB for VoIP). Send IDs, not documents.
{
  "aps": {
    "alert": { "title": "Order shipped", "body": "Arrives Thursday" },
    "sound": "default",
    "thread-id": "order-8812",
    "interruption-level": "active",
    "mutable-content": 1
  },
  "orderId": "8812"
}

interruption-level values: passive, active, time-sensitive (needs the Time Sensitive capability), critical (needs an Apple-granted entitlement).

FCM HTTP v1

The legacy FCM send API is shut down; every send uses POST https://fcm.googleapis.com/v1/projects/<project-id>/messages:send with an OAuth 2 access token from a service account (Firebase Admin SDK handles this).

{
  "message": {
    "token": "<registration token>",
    "data": { "type": "order_shipped", "orderId": "8812" },
    "android": { "priority": "HIGH", "ttl": "3600s", "collapse_key": "order-8812" },
    "apns": {
      "headers": { "apns-priority": "10", "apns-push-type": "alert" },
      "payload": { "aps": { "alert": { "title": "Order shipped", "body": "Arrives Thursday" } } }
    }
  }
}
  • data values must be strings. Nested JSON goes in as an encoded string.
  • High priority is for messages that show something to the user; Android deprioritizes apps whose high-priority messages do not result in a notification.
  • iOS through FCM still needs the APNs key uploaded in the Firebase console.
  • Use topics or conditions for broadcast, and the batch-capable Admin SDK sendEach for per-user fan-out; there is no multicast endpoint in v1 itself.

Token lifecycle

  • Register on every app launch and send the token to the backend when it changes (iOS didRegisterForRemoteNotificationsWithDeviceToken, FCM onNewToken / messaging:didReceiveRegistrationToken).
  • Store (user_id, device_id, platform, environment, token, app_version, updated_at). One row per device, upserted, so reinstalls do not create duplicates.
  • Delete the token on APNs 410 Unregistered or 400 BadDeviceToken, and on FCM UNREGISTERED / NOT_FOUND. Keep it on 429/5xx; retry with backoff.
  • On logout, delete the server row (and call deleteToken on FCM) so the next user on a shared device does not receive the previous user’s messages.
  • Expire tokens not refreshed for 60-90 days.

Permission UX

  • iOS asks once. Show an in-app explanation first, and request only after the user taps a button tied to a concrete benefit (“Notify me when it ships”).
  • Consider provisional authorization (.provisional) to deliver quietly to Notification Center without a prompt; promote to full alerts after the user engages.
  • Android 13+ requires the POST_NOTIFICATIONS runtime permission; after two denials the system stops showing the dialog. Check shouldShowRequestPermissionRationale and offer a Settings deep link instead of re-requesting.
  • Create Android notification channels at startup with user-meaningful names (“Order updates”, “Promotions”); users can mute channels individually, and you cannot raise a channel’s importance after creation.
  • Re-check authorization status on every foreground; users change it in Settings.
let center = UNUserNotificationCenter.current()
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
if granted { await MainActor.run { UIApplication.shared.registerForRemoteNotifications() } }

Handling taps

Route the tap through the same deep-link router the app uses for URLs: put a deeplink string in the payload, parse it in userNotificationCenter(_:didReceive:) or the Android intent extras, and treat it as untrusted input. See mobile-deep-links for the router.

Gotchas

  • Simulator push: xcrun simctl push booted <bundle-id> payload.apns works for local testing but proves nothing about APNs credentials or tokens.
  • A Notification Service Extension has about 30 seconds and a small memory budget; download thumbnails, not videos, and always call the content handler with the best attempt.
  • Badge counts must come from the server’s unread total; incrementing on the device drifts.
  • Firebase on iOS swizzles app delegate methods; if FirebaseAppDelegateProxyEnabled is NO, you must pass the APNs token to Messaging.messaging().apnsToken yourself.
  • Marketing pushes need an opt-out and must respect quiet hours and local time zones.

Verification

  1. Send to one test device per platform and environment (debug and release builds).
  2. Test app states: foreground, background, terminated, and after force-quit on iOS.
  3. Revoke permission in Settings and confirm the app detects it on next foreground.
  4. Uninstall, send, and confirm the backend deletes the token on the error response.
  5. Check delivery metrics: FCM delivery data in the Firebase console; Apple’s Push Notifications Console for APNs delivery logs.

Output

**Provider:** APNs direct | FCM both platforms | other
**Payloads:** <type, push-type/priority, sample JSON per message kind>
**Token store:** <schema, prune rules>
**Permission flow:** <when asked, pre-prompt copy, fallback>
**Tested:** <device, OS, build env, app state> -> delivered / not delivered

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Mobile Push Notifications"
  short_description: "Pushes that arrive, tokens that stay clean"
  default_prompt: "$mobile-push-notifications Push notifications work in debug but never arrive on TestFlight builds"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem