easy-pingv0.8.0

Get in touch

Questions, bug reports, or anything about easy-ping. Either of these reaches me.

Emailteklumo.jembere@gmail.comTelegram@teklumt

For anything others would benefit from, a GitHub issue is better than a DM, because it's searchable.

GitHub

Channels / Mobile push

Edit this page

Mobile push

Native push to iOS and Android through Expo's push service. A token registry, fan-out per device, and receipts that catch uninstalls.

Phones need APNs or FCM, not web push. mobilePush is the channel for them: a plugin that keeps each user's device tokens, and a provider that sends through a push service. expoPush ships first, because Expo's service fronts both APNs and FCM with one token format and no per-platform credentials in development. The client half lives in React Native.

How it works

  1. The app asks the OS for a push token (expo-notifications) and registers it with POST /mobile-push/devices. The token is bound to that account for life.
  2. send() to that user with mobilePush in the channels renders a title, body and data, and the provider sends one batch to every device the user has.
  3. Expo answers with a ticket per message. "Device not registered" prunes the device on the spot. Accepted tickets are kept, and POST /mobile-push/receipts asks Expo later whether APNs or FCM actually delivered; devices that turned out to be gone are pruned then.

A user with no device is skipped, never failed, the same as web push with no browser.

Provider and plugin

notify.ts
import { mobilePush } from "easy-ping/plugins/mobile-push";
import { expoPush } from "easy-ping/providers/expo-push";

const mobile = mobilePush({
  provider: expoPush({ accessToken: process.env.EXPO_ACCESS_TOKEN }),  // token is optional
  render: ({ type, payload }) => {
    const data = payload as { authorName?: string };
    return {
      title: "New reply",
      body: `${data.authorName ?? "Someone"} replied to your comment`,
      data: { screen: "thread" },       // reaches the app's tap handler, with notificationId and type added
      badge: undefined,                  // or a number
      sound: "default",                  // or null for silent
      channelId: "default",              // Android notification channel
    };
  },
});

export const notify = easyPing({
  // ...
  plugins: [mobile],
});

Add "mobilePush" to the channels of every notification type that should reach phones. Options: maxDevicesPerUser (20, evicting the least recently seen), staleAfterDays (180, for the prune route). Expo's access token is only needed if you turned on "enhanced push security" in your Expo account; without it, requests are unauthenticated as Expo allows.

Android credentials

Expo's service reaches Android through Firebase Cloud Messaging, so an Android build needs your Firebase project's google-services.json and the FCM V1 service-account key uploaded with eas credentials. iOS through Expo needs nothing extra in development. Both are Expo's requirements, documented in their push setup guide; the plugin does not change with them.

Registering a device

The app obtains the token and posts it. easy-ping/react-native has the two calls:

App.tsx
import * as Notifications from "expo-notifications";
import { registerMobilePushDevice } from "easy-ping/react-native";

const { data: token } = await Notifications.getExpoPushTokenAsync();
await registerMobilePushDevice({ baseUrl, fetch: authedFetch, token, platform: "ios" });

Registration validates the token's shape (ExponentPushToken[...]), refuses a token another account already registered with a 409, refreshes the row when the same account registers again, and evicts the oldest device past the cap. GET /mobile-push/devices lists a user's devices for a settings screen, with the token's last six characters only. unregisterMobilePushDevice removes one.

Receipts and pruning

Expo accepts a message before APNs or FCM has seen it, so an uninstalled app is often reported only in the receipt, minutes later. Schedule the machine route next to your cron:

POST /api/notifications/mobile-push/receipts
Authorization: Bearer $NOTIFY_CRON_SECRET     # or machineSecret

It fetches receipts for up to 1000 pending tickets, deletes devices reported gone, and forgets tickets older than a day, which is as long as Expo keeps them. POST /mobile-push/prune removes devices not seen for staleAfterDays.

Ticket results are classified like every other provider: DeviceNotRegistered prunes, MessageRateExceeded retries, MessageTooBig and bad credentials fail without retry, and a 5xx or network error retries the whole batch. Expo's access token never appears in an error.

Routes and tables

RouteMethodScopeDoes
/mobile-push/devicesPOSTuserRegister { token, platform, deviceName? }; 400 invalid, 409 owned elsewhere
/mobile-push/devicesGETuserThe user's devices, tokens truncated
/mobile-push/devices/removePOSTuserRemove one by { token }
/mobile-push/receiptsPOSTmachineCheck pending tickets, prune gone devices
/mobile-push/prunePOSTmachineRemove devices not seen for staleAfterDays

Two tables from mobilePushSchema: notification_mobile_push_device (unique on token) and notification_mobile_push_ticket. Create them like push devices.

Other services

The plugin talks to a MobilePushProvider: isValidToken, send(messages) returning one ticket per message, and an optional receipts(ids). A direct FCM HTTP v1 or APNs provider is that interface plus the service's auth; the plugin, tables and routes do not change. Expo is the one that ships today.