Push setup (iOS & Android)

This guide gets your Expo / React Native app receiving poolse's pushes: call rings, chat notifications and missed calls. It follows the setup of Loop, poolse's reference app. Read Push notifications first for how the parts fit.

You will:

  1. Upload your app's push credentials on the dashboard.
  2. Add @poolse/native and the right iOS capabilities to your app.
  3. Register each device's tokens with poolse.
  4. Check it with a test push, and choose the notification settings.

1. Credentials

iOS: an APNs auth key

  1. In the Apple Developer portal, under Certificates, Identifiers & Profiles → Keys, create a key with Apple Push Notifications service (APNs) enabled. Download the .p8 file. Apple only lets you download it once.
  2. Note the Key ID (10 characters) and your Team ID (10 characters, top right of the portal).
  3. On the poolse dashboard, open Push → Credentials → iOS. Enter:
    • the Key ID and Team ID;
    • the bundle id exactly as your app has it, such as com.acme.chat. Do not add .voip; poolse adds it for VoIP pushes;
    • the environment: production for TestFlight and App Store builds, sandbox for development builds;
    • the contents of the .p8 file.

The key is stored encrypted and is never shown again. To replace the other fields later, you can leave the key blank to keep the stored one.

Android: a Firebase service account

  1. In the Firebase console, open the project your app's google-services.json comes from.
  2. Go to Project settings → Service accounts and click Generate new private key. You get a JSON file with type, project_id, client_email and private_key.
  3. On the dashboard, open Push → Credentials → Android and upload or paste that file. poolse checks the key with Google before saving it.

This is not google-services.json. That file goes in your app. The service account stays with poolse.

2. The app

Install the packages. Expo prebuild needs Node 20.19 or later.

npx expo install @poolse/native expo-notifications

@poolse/native needs react, react-native, expo and expo-notifications as peers.

app.json

{
  "expo": {
    "ios": {
      "bundleIdentifier": "com.acme.chat",
      "infoPlist": {
        "NSMicrophoneUsageDescription": "Talk on voice and video calls.",
        "NSCameraUsageDescription": "Show your camera on a video call.",
        "UIBackgroundModes": ["audio", "voip", "remote-notification"]
      },
      "entitlements": { "aps-environment": "production" }
    },
    "android": {
      "package": "com.acme.chat",
      "googleServicesFile": "./google-services.json",
      "permissions": [
        "android.permission.RECORD_AUDIO",
        "android.permission.MODIFY_AUDIO_SETTINGS",
        "android.permission.CAMERA",
        "android.permission.BLUETOOTH"
      ]
    },
    "plugins": [
      "expo-notifications",
      ["@poolse/native", { "ringback": "./assets/sounds/ringback.mp3" }]
    ]
  }
}
  • List @poolse/native after expo-notifications.
  • UIBackgroundModes needs voip for PushKit, audio to keep a call going when the screen locks, and remote-notification for the read sync.
  • aps-environment must match the environment of the tokens the build registers: development builds talk to sandbox. Loop keeps one value in its config and passes it both to the entitlement and to configure({ apnsEnvironment }), so the two cannot drift.
  • ringback is optional: the tone a caller hears while it rings. Its file name may only use lowercase letters, digits and underscores.

Plugin options:

OptionDefaultWhat it does
ringbacknoneA sound file bundled on both platforms. Pass its base name to configure({ ringback }).
pictureInPicturetrueAndroid: a video call can float as picture-in-picture.
lockscreentrueAndroid: the app shows over the lock screen when opened for a call.

Then rebuild the native projects:

npx expo prebuild --clean

@poolse/native is native code, so it does not run in Expo Go. Use a development build.

Configure the native core

Call configure once, early, before anything else in @poolse/native:

import * as calls from '@poolse/native';

await calls.configure('https://api.poolse.dev', {
  ringback: 'ringback',          // base name of the plugin's ringback file
  apnsEnvironment: 'production', // iOS: must match aps-environment
});

3. Register the device

Register once a user is signed in, and again whenever the OS rotates a token. Registering is idempotent. Retry on failure: until a device is registered, nothing can ring it.

iOS

An iPhone needs two tokens.

import * as calls from '@poolse/native';
import * as Notifications from 'expo-notifications';

// The PushKit (VoIP) token — rings only. @poolse/native owns PushKit.
function registerVoip(voip: calls.VoipToken | null) {
  if (!voip?.token) return;
  void poolse.devices.register({
    token: voip.token,
    platform: 'ios',
    environment: voip.environment === 'sandbox' ? 'sandbox' : 'production',
    kind: 'voip',
  });
}
registerVoip(calls.getVoipToken());
calls.onVoipToken(registerVoip);

// The regular APNs token — chat notifications, missed calls, read sync.
await Notifications.setNotificationCategoryAsync('poolse_missed_call', [
  { identifier: 'call_back', buttonTitle: 'Call back', options: { opensAppToForeground: true } },
]);
const perm = await Notifications.requestPermissionsAsync();
if (perm.granted) {
  const { data: token } = await Notifications.getDevicePushTokenAsync();
  await poolse.devices.register({
    token,
    platform: 'ios',
    environment: 'production', // the same value as aps-environment
    kind: 'alert',
  });
}

Android

One token, from FCM:

const perm = await Notifications.requestPermissionsAsync();
if (perm.granted) {
  const { data: token } = await Notifications.getDevicePushTokenAsync();
  await poolse.devices.register({ token, platform: 'android' });
}
Notifications.addPushTokenListener(({ data }) => {
  void poolse.devices.register({ token: data, platform: 'android' });
});

On Android 14 and later, showing an incoming call over the lock screen needs the full-screen notifications permission, which is off by default for many apps. Check it with calls.canUseFullScreenIntent() and send the user to it with calls.openFullScreenIntentSettings().

Sign-out

Unregister on sign-out, or the phone keeps getting the previous user's notifications:

await poolse.devices.unregister(token);

Chat notifications on screen

Tell the core which conversation is open, so it does not notify for the chat the user is reading:

calls.setActiveConversation(conversationId); // opened a chat
calls.setActiveConversation(null);           // left it, or app went to background

On iOS, let expo-notifications decide with the same rule:

Notifications.setNotificationHandler({
  handleNotification: async (n) => {
    const show = calls.shouldPresentMessage(n.request.content.data);
    return { shouldShowBanner: show, shouldShowList: show, shouldPlaySound: show, shouldSetBadge: true };
  },
});

Reply and Mark as read run in native code, with the app closed. On a tap, find the conversation to open with calls.conversationOfNotification(data). On Android, a tap on a missed call arrives as calls.onCallAction(cb) / calls.takeCallAction(), with type: 'open' | 'call_back'.

4. Test, then choose the settings

On the dashboard, Push → Overview shows whether each platform is connected and how many devices are registered. Send a test pushes to one user's devices and shows the result per device.

Then set the workspace's behaviour under Push → Notification settings:

  • Send chat notifications: on or off.
  • What a notification shows: sender and message, sender only, or nothing.
  • Group conversations: every message, or only @mentions.
  • Missed-call notifications: on or off.

Troubleshooting

SymptomCheck
Nothing arrives on iOSThe bundle id on the dashboard. The aps-environment of the build vs. the environment registered. That both tokens are registered.
Calls ring only while the app is openThe VoIP token is registered with kind: "voip", and voip is in UIBackgroundModes.
Android shows a notification instead of a call screen on a locked phoneThe full-screen notifications permission.
Users get two notifications per messageYour backend also sends notifications from message.created. Turn one off.
A signed-out user still gets notificationsCall poolse.devices.unregister(token) on sign-out.
Push setup (iOS & Android) · Guides · poolse docs