A lightweight macOS menu bar utility for quickly testing Expo push notifications, Live Activity updates, native APNs, and FCM (Android) pushes
- Send test push notifications to your Expo apps directly from the menu bar
- Send Live Activity pushes (start, update, end) directly to APNs
- Simple and intuitive interface for quick testing
- Easy configuration of notification payload and options
- Rich content (image) support for Android notifications via
richContent - Advanced push notification features (priority, interruption level, TTL, and more)
- Platform-specific settings for iOS and Android
- Color pickers, progress sliders, and date pickers for Live Activity content
- JSON import/export for Live Activity payloads
- APNs JWT authentication with .p8 key files (no third-party dependencies)
- Native APNs push directly to raw device tokens β full payload control, image URL support
- Native FCM push directly to Firebase Cloud Messaging HTTP v1 API β service account auth, no SDKs required
The easiest way to get QuickPush is from the Mac App Store. One-time purchase, automatic updates, no setup required.
QuickPush is open source. If you prefer, you can clone the repo and build it locally with Xcode:
git clone https://github.com/betomoedano/quick-push.git
open quick-push/QuickPush.xcodeprojRequires Xcode 16+ and macOS 14.6+.
To send Live Activity push notifications, QuickPush communicates directly with Apple's APNs (Apple Push Notification service) using JWT-based authentication. You'll need a few things from your Apple Developer account before you can start.
Your Team ID is a 10-character string that identifies your Apple Developer team.
- Go to Apple Developer Account
- Sign in with your Apple ID
- Your Team ID is displayed under Membership Details
The .p8 key file is used to sign JWT tokens that authenticate your requests with APNs. You only need to create this once β it works for all your apps.
- Go to Certificates, Identifiers & Profiles > Keys
- Click the + button to create a new key
- Give it a name (e.g. "QuickPush APNs Key")
- Check Apple Push Notifications service (APNs)
- Click Continue, then Register
- Download the .p8 file β you can only download it once, so save it somewhere safe
- Note the Key ID shown on this page (10-character string, e.g.
ABC123DEF4)
Important: Apple only lets you download the .p8 file once. If you lose it, you'll need to create a new key.
For more details, see Apple's documentation: Establishing a Token-Based Connection to APNs
The Bundle ID is the unique identifier for your app (e.g. com.yourcompany.yourapp).
- Go to Certificates, Identifiers & Profiles > Identifiers
- Find your app in the list
- The Bundle ID is shown next to each app
This must match the bundle identifier of the app that registered the Live Activity.
- Open QuickPush from your menu bar
- Switch to the Live Activity tab
- Expand the APNs Configuration section
- Enter your Team ID, Key ID, and Bundle ID
- Click Browse... to select your
.p8key file - Choose Sandbox (for development/TestFlight builds) or Production (for App Store builds)
Your configuration is saved automatically and persists between sessions.
- Choose an event type:
- Start β creates a new Live Activity on the device (requires a push-to-start token)
- Update β updates an existing Live Activity (requires an activity token)
- End β ends an existing Live Activity (requires an activity token)
- Paste the device token (hex string) from your app
- Fill in the content state fields (title, subtitle, progress, etc.)
- For Start events, configure the attributes (colors, layout options)
- Click Send
| Environment | When to use | APNs hostname |
|---|---|---|
| Sandbox | Development builds, TestFlight | api.sandbox.push.apple.com |
| Production | App Store releases | api.push.apple.com |
If you're testing on a device with a development or TestFlight build, use Sandbox. If your app is installed from the App Store, use Production.
Your app needs to provide the device token for Live Activities. Depending on the event type:
- Push-to-start token: Obtained via
Activity.pushToStartTokenβ use this for Start events - Activity token: Obtained via
activity.pushTokenafter a Live Activity has been started β use this for Update and End events
These are raw APNs hex tokens (not Expo push tokens). See the Expo LiveActivity documentation for implementation details with expo-live-activity.
Click the JSON button in the Live Activity tab to:
- Export the current form as a JSON payload (useful for debugging or sharing)
- Import a JSON payload to populate the form fields (useful for quickly loading saved payloads)
The APNs tab lets you send native iOS push notifications directly to Apple's Push Notification service β no Expo push token required. It's useful for testing notifications on devices where you only have the raw APNs device token, or when you need full control over the APNs payload.
APNs device tokens are raw hex strings (64 hex characters), different from Expo push tokens (ExponentPushToken[...]). Your app receives this token from the system:
// Swift (iOS)
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
print("APNs token:", token)
}Tokens can be saved across sessions using the save button next to the token field.
| Field | Description |
|---|---|
| Title / Subtitle / Body | Standard alert text |
| Sound | default or a custom sound file name bundled in the app |
| Badge | Number shown on the app icon |
| Image URL | URL of an image to attach to the notification (see below) |
| Thread ID | Groups related notifications together in the notification center |
| Category | Enables interactive notification actions registered in the app |
| Interruption Level | Controls delivery timing (Active, Passive, Time Sensitive, Critical) |
| Mutable Content | Allows a Notification Service Extension to modify the payload before display |
| Content Available | Wakes the app in the background to process the notification silently |
| Priority | 10 = immediate, 5 = normal/background |
APNs does not have a built-in image field. To display an image in a notification, the iOS app must include a Notification Service Extension β a small app extension that intercepts the push before it's shown, downloads the image, and attaches it as a media attachment.
The Image URL field in QuickPush injects the URL into the payload under a custom key path:
{
"aps": { "alert": { "title": "Hello" }, "mutable-content": 1 },
"body": { "_richContent": { "image": "https://example.com/photo.jpg" } }
}Your Notification Service Extension would read it like this:
// In NotificationService.swift
let imageUrl = (request.content.userInfo["body"] as? [String: Any])
.flatMap { $0["_richContent"] as? [String: Any] }
.flatMap { $0["image"] as? String }
.flatMap { URL(string: $0) }Note: The exact key path (
body._richContent.image) is the convention QuickPush uses. The key name your app listens for depends entirely on what the Notification Service Extension in that app is coded to read. If you're testing against an app that uses a different key, use the Custom Data section to inject the key manually instead.
Filling in the Image URL field automatically enables Mutable Content in the payload, which is required for the Notification Service Extension to be invoked.
The FCM tab lets you send native Android push notifications directly to Firebase Cloud Messaging's HTTP v1 API β no Expo push token required. It authenticates using a Firebase service account, signs OAuth 2.0 tokens locally with RS256 (no third-party SDKs), and supports both notification and data-only messages.
If you don't already have one:
- Go to the Firebase Console
- Click Add project and follow the setup wizard
- Once created, add your Android app to the project (Project settings β Your apps β Add app)
QuickPush authenticates with FCM using a service account JSON file. This is the same credential type used by Firebase Admin SDKs.
- In the Firebase Console, open your project
- Go to Project settings (gear icon) β Service accounts tab
- Click Generate new private key
- Confirm by clicking Generate key β a
.jsonfile will download
The JSON file contains your project ID, client email, and private key. QuickPush reads all three from the file automatically when you select it.
Keep this file secure. Anyone with it can send push notifications to all users of your app. Don't commit it to version control.
The FCM registration token identifies a specific app install on a device. It's not the same as an Expo push token.
React Native / Expo (using @react-native-firebase/messaging):
import messaging from '@react-native-firebase/messaging';
const token = await messaging().getToken();
console.log('FCM token:', token);React Native / Expo (using expo-notifications):
import * as Notifications from 'expo-notifications';
const { data: token } = await Notifications.getDevicePushTokenAsync();
// On Android this returns the raw FCM registration token
console.log('FCM token:', token);FCM tokens are long base64url strings (~163+ characters). They can change when the user reinstalls the app or clears app data.
- Open QuickPush from your menu bar
- Switch to the FCM tab (β4)
- Expand the FCM Configuration section
- Click Browse... and select your downloaded service account
.jsonfile - Project ID and Client Email are auto-filled from the file β verify they look correct
Your configuration is saved automatically and persists between sessions. The service account JSON contents are stored in macOS user defaults (local to your machine, not synced).
- Paste your FCM registration token into the token field (or use the save button to store it for future sessions)
- Choose the Message Type:
- Notification β displays a visible notification on the device
- Data β delivers a silent data payload to the app; no notification UI is shown
- Fill in the notification fields (Title, Body, Image URL, Channel ID, Sound, Color)
- Optionally add Custom Data key-value pairs β these are delivered in the message's
datablock - Click Send (or ββ΅)
| Type | Description | When to use |
|---|---|---|
| Notification | Shows a visible notification with title, body, and optional image | User-facing alerts |
| Data | Silent payload delivered to the app; no system UI | Background processing, in-app messages |
| Field | Description |
|---|---|
| Title | Title of the notification |
| Body | Main message content |
| Image URL | URL of an image displayed in the expanded notification |
| Channel ID | Android notification channel the app must have pre-created (defaults to default) |
| Sound | Sound to play β use default for the device default |
| Color | Accent color for the notification icon, in hex format (e.g. FF5733) |
| Value | Description |
|---|---|
| HIGH | Wakes the device immediately β use for user-visible notifications |
| NORMAL | May be delayed for battery optimization β suitable for non-urgent data messages |
| Code | Meaning |
|---|---|
INVALID_ARGUMENT |
The request payload is malformed or the token format is wrong |
NOT_FOUND / UNREGISTERED |
The token is no longer valid; the app was uninstalled or the token expired |
SENDER_ID_MISMATCH |
The token was registered with a different Firebase project |
QUOTA_EXCEEDED |
Sending rate too high; retry with exponential backoff |
UNAVAILABLE |
FCM service temporarily unavailable; retry with exponential backoff |
INTERNAL |
Internal FCM server error; retry with exponential backoff |
Click the cURL button to generate a ready-to-run curl command for the current configuration. Because the OAuth access token requires an async exchange with Google's servers, the cURL output uses an <ACCESS_TOKEN> placeholder. To fill it in:
# Using the Google Cloud CLI
gcloud auth print-access-token
# Or using the service account directly
gcloud auth activate-service-account --key-file=/path/to/service-account.json
gcloud auth print-access-tokenReplace <ACCESS_TOKEN> in the copied curl command with the token output.
Click the save icon (β) next to any token to store it with a label. Saved tokens persist across app restarts and appear at the top of the token list with a toggle to include or exclude them from sends.
QuickPush supports the richContent field, allowing you to attach an image to your push notifications.
- Open the Push Notification tab
- Expand Advanced Settings
- In the Common Settings section, find the Image (richContent) field
- Paste the URL of the image you want to display
| Platform | Support |
|---|---|
| Android | Works out of the box β the image will display in the notification |
| iOS | Requires a Notification Service Extension target in your app to process and display the image |
Tip: The help tooltip for this field includes a clickable link to the Expo PR with an iOS implementation example.
MIT
Made with β€οΈ by codewithbeto.dev