TechnicalApril 2026 · 12 min read

Push Notifications in Expo: The 2026 Setup Guide

Push notifications are the highest-leverage retention lever in a mobile app — and one of the fastest ways to get a React Native project stuck for a week. This guide walks through the clean 2026 Expo setup: local notifications, remote via the Expo Push API, token management, background delivery, and the subtle gotchas.

Quick path

Install expo-notifications, create a dev build via EAS, upload APNs key + FCM credentials in EAS credentials, request permission, store the Expo push token, and send from your server via the Expo Push API. Expect 1–2 hours end-to-end on a clean project.

Local vs remote: pick the right one

Not every app needs a backend to send notifications. Two categories:

  • Local notifications: scheduled by the app itself — daily reminders, habit streaks, timers. No server. Works in Expo Go. Use for solo-user apps where reminders are the core feature (habit trackers, meditation, fitness).
  • Remote notifications: triggered by your backend — new messages, order updates, social notifications. Requires APNs and FCM credentials plus a development build. Use whenever an event happens server-side.

Prerequisites before you write code

  • Apple Developer account with a registered App ID and an APNs Auth Key (.p8 file from developer.apple.com)
  • A Firebase project with the Android app registered and google-services.json downloaded
  • An Expo project using EAS Build (Expo Go is insufficient for remote push)
  • A physical iOS device for testing — simulators cannot receive remote notifications

Step 1: install and configure

npx expo install expo-notifications expo-device

# app.json plugins:
# "plugins": [["expo-notifications", {
#   "icon": "./assets/notification-icon.png",
#   "color": "#fb923c"
# }]]

# Upload credentials to EAS:
eas credentials

# Pick iOS -> Push Notifications -> add APNs key
# Pick Android -> FCM V1 -> add google-services.json

# Build a development client:
eas build --profile development --platform all

Install the dev client on a physical device. Run npx expo start --dev-client instead of Expo Go.

Step 2: request permission and get the push token

import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';

async function registerForPush() {
  if (!Device.isDevice) return;
  const { status: existing } = await
    Notifications.getPermissionsAsync();
  let final = existing;
  if (existing !== 'granted') {
    const { status } = await
      Notifications.requestPermissionsAsync();
    final = status;
  }
  if (final !== 'granted') return;

  const token = (await
    Notifications.getExpoPushTokenAsync({
      projectId: process.env.EXPO_PUBLIC_PROJECT_ID,
    })).data;

  // Send token to your backend:
  await fetch('/api/push-tokens', {
    method: 'POST',
    body: JSON.stringify({ token }),
  });
}

Call registerForPush() once the user is authenticated — not on app launch. Users say yes more often when the ask comes after a clear value exchange.

Step 3: send from your server via Expo Push API

// Node.js with expo-server-sdk:
import { Expo } from 'expo-server-sdk';
const expo = new Expo();

const messages = [{
  to: userPushToken,
  sound: 'default',
  title: 'New message',
  body: 'Alex sent you a message.',
  data: { screen: 'Chat', chatId: '42' },
}];

const chunks = expo.chunkPushNotifications(messages);
for (const chunk of chunks) {
  await expo.sendPushNotificationsAsync(chunk);
}

The Expo Push API handles APNs and FCM routing for you. Chunking matters at scale — the API accepts up to 100 messages per request and rate-limits above that.

Step 4: handle notification taps

import * as Notifications from 'expo-notifications';
import { useEffect } from 'react';
import { router } from 'expo-router';

useEffect(() => {
  const sub = Notifications.
    addNotificationResponseReceivedListener(r => {
      const data = r.notification.request.content.data;
      if (data.screen === 'Chat')
        router.push(`/chat/${data.chatId}`);
    });
  return () => sub.remove();
}, []);

Deep-link directly to the relevant screen. Dropping users at the home screen after tapping a notification is a classic retention killer.

Common pitfalls

  • Testing on simulator: iOS simulators cannot receive remote notifications. Use a physical device.
  • Asking for permission on launch: conversion tanks. Ask after the user completes a first meaningful action.
  • Token churn: push tokens change when users reinstall the app. Treat them as rotating — update your backend on every launch.
  • Android battery savers can silently kill background delivery. OEM-specific (Xiaomi, OnePlus are the worst). Document this in your support docs.
  • Not polling receipts: the Expo Push API returns receipt IDs that tell you delivery status 15+ minutes later. Poll them and clean up invalid tokens.

Shortcut: generate an app with push wired in

If you use ShipNative, describe your notification scenarios in the initial prompt (“send daily reminder,” “notify on new comment,” etc.) and the generated Expo project includes expo-notifications setup and the permission flow. You still need to upload APNs and FCM credentials yourself, but the JavaScript plumbing arrives ready.

Frequently Asked Questions

Do push notifications work in Expo Go?

Remote push does not work reliably in Expo Go. You need a development build or EAS Build for testing remote notifications end-to-end. Local notifications do work in Expo Go and are a valid path for MVP reminder apps that do not need a server.

Should I use the Expo Push API or raw APNs/FCM?

Use the Expo Push API for 95% of cases. It abstracts APNs (iOS) and FCM (Android) behind one endpoint, handles token rotation, and is free up to typical indie-app volume. Drop to raw APNs/FCM only if you have enterprise constraints or need custom payload features Expo does not expose.

Why am I not receiving notifications in the background?

Three common causes: (1) app is killed on Android by aggressive battery savers — you need to whitelist it, (2) iOS requires content-available:true for silent pushes and the payload must be specific, (3) permissions were never granted. Check device Settings first.

How do I send push notifications at scale?

Run a server-side job that batches up to 100 expo-push-tokens per request to the Expo Push API. Store the resulting receipt IDs and check them 15+ minutes later to confirm delivery. For 100k+ users, the batching and receipt-polling matter — do not send from the client.

Can I send push notifications to iOS and Android with one code path?

Yes. The expo-server-sdk (Node, Python, Ruby) takes an expo-push-token plus a title and body, and handles iOS/Android routing internally. One code path for both platforms is the main reason to use Expo over raw APNs/FCM.

React Native Authentication 2026

Tie push tokens to user accounts the right way.

Read guide →

Expo EAS Submission Checklist

Verify push works before App Store review.

See checklist →

Ship a real React Native app today

Describe, preview, and export Expo code — free to start.

Build with ShipNative →