native-push-sender
v0.0.1
Published
Send FCM (Android) and APNs (iOS) native push notifications from Cloudflare Workers without firebase-admin. Zero-dependency FCM HTTP v1 + APNs HTTP/2 sender using Web Crypto JWT signing. FCM also works on Node.js.
Maintainers
Readme
native-push-sender
Send FCM (Android) and APNs (iOS) native push notifications from Cloudflare Workers without firebase-admin. A tiny, zero-dependency sender for the FCM HTTP v1 API and the APNs HTTP/2 provider API that signs its own JWTs with the Web Crypto API, so it runs on the workerd runtime where firebase-admin cannot.
- Cloudflare Workers (workerd): FCM and APNs both work.
- Node.js 18+: FCM works. APNs does not, because Node's built-in
fetchspeaks HTTP/1.1 only and APNs requires HTTP/2.
Why
firebase-admin depends on Node-only modules (grpc, fs), so it does not run on Cloudflare Workers. This package signs the Google service account JWT (FCM, RS256) and the APNs provider token JWT (ES256) directly with Web Crypto and sends plain REST requests with fetch.
It is not tied to any database, framework, or request context. You pass in secrets (NativePushEnv) and a payload (NativePushPayload); fan-out, token storage, and cleanup stay in your app.
Use cases
- Send task reminders or chat notifications to Android and iOS apps (e.g. Capacitor, React Native, native) from a Cloudflare Workers API.
- Add custom action buttons ("Done", "Snooze") to notifications via
actionCategory. - Dismiss a notification on a user's other Android devices once it has been handled on one (
sendFcmDismiss). - Clean up dead device tokens when a send reports
expired.
Install
npm install native-push-senderUsage
import { sendFcm, sendApns, sendFcmDismiss } from "native-push-sender";
const env = {
FCM_PRIVATE_KEY: process.env.FCM_PRIVATE_KEY, // Google service account key JSON, as a string
APNS_KEY_ID: process.env.APNS_KEY_ID,
APNS_TEAM_ID: process.env.APNS_TEAM_ID,
APNS_PRIVATE_KEY: process.env.APNS_PRIVATE_KEY, // contents of the .p8 file (PEM)
APNS_BUNDLE_ID: process.env.APNS_BUNDLE_ID,
};
// Android (FCM)
const result = await sendFcm(env, {
token: androidFcmToken,
payload: {
title: "Title",
body: "Body",
url: "/tasks/123",
tag: "task-123",
// Set actionCategory for custom action buttons:
// - FCM: sent as a data-only, high-priority message
// - APNs: set as aps.category
actionCategory: "MY_ACTIONS",
// Arbitrary key-value data for your native app
data: { taskId: "123", kind: "reminder" },
},
});
// iOS (APNs) — Cloudflare Workers only
const result = await sendApns(env, { token: apnsDeviceToken, payload });
// Dismiss a notification on other devices (Android only, FCM data-only)
await sendFcmDismiss(env, { token: androidFcmToken, tag: "task-123" });
if (!result.ok) {
if (result.expired) {
// The token is invalid — remove it from storage (FCM 404/UNREGISTERED, APNs 410/BadDeviceToken).
}
console.error(result.status, result.bodyText);
}Missing secrets are not thrown as errors: if the FCM or APNs fields are not set, that sender returns ok: false.
Access tokens are cached in memory (FCM OAuth token for 1 hour, APNs provider token for 55 minutes), so most sends on a warm Workers isolate skip the token round trip.
API
sendFcm(env, { token, payload }) → Promise<PushSendResult>
Sends a notification to an Android device through FCM HTTP v1. If payload.actionCategory is set, it is sent as a data-only, high-priority message.
sendApns(env, { token, payload }) → Promise<PushSendResult>
Sends a notification to an iOS device through APNs HTTP/2. If payload.actionCategory is set, it goes into aps.category. Requires an HTTP/2-capable fetch (Cloudflare Workers).
sendFcmDismiss(env, { token, tag }) → Promise<PushSendResult>
Sends an FCM data-only message telling the app to dismiss the notification with the given tag. Use it to clear the tray on other devices after one device handled the notification.
The message carries data: { type: "dismiss", tag }; your app's FirebaseMessagingService must handle it and cancel the matching notification.
parseServiceAccount(env) → ServiceAccount | null
Parses env.FCM_PRIVATE_KEY into a service account object. Returns null if it is not set.
Types
| Name | Description |
|---|---|
| NativePushEnv | Secret env subset (FCM + APNs fields, all optional) |
| NativePushPayload | Notification payload (title/body/url/tag/actionCategory/data) |
| NativePushInput | { token, payload } |
| NativePushSender | Shared signature of sendFcm / sendApns |
| PushSendResult | { ok, expired, status, bodyText } |
Build
Built with obuild. prepare runs obuild --stub, which writes a stub dist that points at the source, so local consumers can use it without a build. Run pnpm build:dist for a real build.
License
MIT
