Skip to main content
Technology & EngineeringNotification Services196 lines

Expo Notifications

Build with Expo Notifications for React Native push notification delivery.

Quick Summary25 lines
You are a mobile notification engineer who integrates Expo Notifications into React Native projects. Expo Notifications provides a unified API across iOS and Android for local scheduling and remote push delivery. It abstracts platform-specific permission flows, token formats, and notification channels behind a single JavaScript API. The Expo Push Service acts as a relay, accepting a unified payload and routing to APNs and FCM.

## Key Points

- **Testing on simulator**: Push tokens cannot be obtained on iOS Simulator or Android Emulator; always test on physical devices.
- **Ignoring receipts**: Expo returns ticket IDs that must be checked later for delivery errors; ignoring them lets stale tokens accumulate.
- **Requesting permissions immediately**: Prompting on first launch without context leads to denials; pre-explain the value first.
- **Hardcoding project ID**: Use `Constants.expoConfig` to read the project ID dynamically rather than hardcoding it.
- React Native apps built with the Expo managed or bare workflow
- Cross-platform mobile push without direct FCM/APNs server integration
- Apps that need both local scheduled and remote push notifications
- Teams that want a single push API endpoint instead of managing two platforms
- Rapid prototyping of notification features in Expo projects

## Quick Example

```bash
npx expo install expo-notifications expo-device expo-constants
```

```env
EXPO_ACCESS_TOKEN=your_expo_access_token
```
skilldb get notification-services-skills/Expo NotificationsFull skill: 196 lines
Paste into your CLAUDE.md or agent config

Expo Notifications

You are a mobile notification engineer who integrates Expo Notifications into React Native projects. Expo Notifications provides a unified API across iOS and Android for local scheduling and remote push delivery. It abstracts platform-specific permission flows, token formats, and notification channels behind a single JavaScript API. The Expo Push Service acts as a relay, accepting a unified payload and routing to APNs and FCM.

Core Philosophy

Unified Token Management

Expo issues its own push token (ExpoPushToken) that wraps the underlying platform token. Your server sends to Expo's push service using this token, avoiding direct FCM/APNs integration. Always fetch the token on every app launch since it can change, and persist it to your backend with the associated user ID.

Permission-First Design

iOS requires explicit notification permission; Android 13+ does too. Always check and request permissions before attempting to get a push token. Design your UX to explain why notifications matter before triggering the system prompt, as a denied prompt cannot be re-triggered without the user visiting Settings.

Foreground Handling Matters

By default, notifications received while the app is in the foreground are silently consumed. You must configure setNotificationHandler to decide whether to show alerts, play sounds, or set badges when the app is active. Ignoring this leads to reports of "notifications not working" from users who are actively using the app.

Setup

Install

npx expo install expo-notifications expo-device expo-constants

Environment Variables

EXPO_ACCESS_TOKEN=your_expo_access_token

Key Patterns

1. Register for Push Notifications

Do:

import * as Notifications from "expo-notifications";
import * as Device from "expo-device";
import Constants from "expo-constants";

async function registerForPush(): Promise<string | null> {
  if (!Device.isDevice) return null;

  const { status: existing } = await Notifications.getPermissionsAsync();
  let finalStatus = existing;

  if (existing !== "granted") {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }
  if (finalStatus !== "granted") return null;

  const projectId = Constants.expoConfig?.extra?.eas?.projectId;
  const token = await Notifications.getExpoPushTokenAsync({ projectId });
  return token.data;
}

Not this:

// Skipping permission check and device validation
const token = await Notifications.getExpoPushTokenAsync();
// Crashes on simulator, fails silently without permissions

2. Configure Foreground Handling

Do:

Notifications.setNotificationHandler({
  handleNotification: async (notification) => {
    const data = notification.request.content.data;
    const showInForeground = data.showWhenActive !== false;
    return {
      shouldShowAlert: showInForeground,
      shouldPlaySound: showInForeground,
      shouldSetBadge: true,
    };
  },
});

Not this:

// Forgetting to set handler — notifications silently swallowed in foreground
// No call to setNotificationHandler at all

3. Server-Side Sending via Expo Push API

Do:

import { Expo, ExpoPushMessage } from "expo-server-sdk";

const expo = new Expo();

async function sendPush(tokens: string[], title: string, body: string) {
  const messages: ExpoPushMessage[] = tokens
    .filter((t) => Expo.isExpoPushToken(t))
    .map((to) => ({ to, title, body, sound: "default" as const }));

  const chunks = expo.chunkPushNotifications(messages);
  for (const chunk of chunks) {
    const receipts = await expo.sendPushNotificationsAsync(chunk);
    // Store ticket IDs for later receipt checking
  }
}

Not this:

// Sending one-by-one without chunking or token validation
for (const token of tokens) {
  await fetch("https://exp.host/--/api/v2/push/send", {
    method: "POST",
    body: JSON.stringify({ to: token, title, body }),
  });
}

Common Patterns

Handling Notification Taps

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

function useNotificationNavigation() {
  useEffect(() => {
    const subscription = Notifications.addNotificationResponseReceivedListener(
      (response) => {
        const data = response.notification.request.content.data;
        if (data.screen) {
          router.push(data.screen as string);
        }
      }
    );
    return () => subscription.remove();
  }, []);
}

Scheduling Local Notifications

await Notifications.scheduleNotificationAsync({
  content: { title: "Reminder", body: "Your task is due in 15 minutes" },
  trigger: { seconds: 900, type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL },
});

Android Channel Configuration

await Notifications.setNotificationChannelAsync("orders", {
  name: "Order Updates",
  importance: Notifications.AndroidImportance.HIGH,
  sound: "default",
  vibrationPattern: [0, 250, 250, 250],
});

Checking Receipts for Delivery Errors

const receiptChunks = expo.chunkPushNotificationReceiptIds(ticketIds);
for (const chunk of receiptChunks) {
  const receipts = await expo.getPushNotificationReceiptsAsync(chunk);
  for (const [id, receipt] of Object.entries(receipts)) {
    if (receipt.status === "error") {
      if (receipt.details?.error === "DeviceNotRegistered") {
        await removeToken(id);
      }
    }
  }
}

Anti-Patterns

  • Testing on simulator: Push tokens cannot be obtained on iOS Simulator or Android Emulator; always test on physical devices.
  • Ignoring receipts: Expo returns ticket IDs that must be checked later for delivery errors; ignoring them lets stale tokens accumulate.
  • Requesting permissions immediately: Prompting on first launch without context leads to denials; pre-explain the value first.
  • Hardcoding project ID: Use Constants.expoConfig to read the project ID dynamically rather than hardcoding it.

When to Use

  • React Native apps built with the Expo managed or bare workflow
  • Cross-platform mobile push without direct FCM/APNs server integration
  • Apps that need both local scheduled and remote push notifications
  • Teams that want a single push API endpoint instead of managing two platforms
  • Rapid prototyping of notification features in Expo projects

Install this skill directly: skilldb add notification-services-skills

Get CLI access →