Skip to main content
Technology & EngineeringNotification Services195 lines

Expo Notifications

Build with Expo Notifications for React Native push notification delivery. Covers device token registration, local and remote notifications, notification handlers, and the Expo Push API for server-side sending.

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: 195 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 →

Related Skills

Firebase Cloud Messaging

Integrate Firebase Cloud Messaging (FCM) for cross-platform push notifications. Covers token management, topic subscriptions, data vs notification messages, and server-side sending via the Admin SDK.

Notification Services170L

Knock

Implement Knock notification infrastructure for multi-channel delivery. Covers workflow orchestration, in-app feeds, email, push, SMS, and Slack channels with preference management and batching.

Notification Services175L

Magicbell

Build with MagicBell for embeddable notification inboxes and multi-channel delivery. Covers the notification inbox UI component, real-time WebSocket updates, server-side sending, user preferences, and category management.

Notification Services211L

Notifee

Integrate Notifee for robust local notification management within your React Native applications. This service provides a comprehensive cross-platform API for displaying, interacting with, and managing notifications, enhancing user engagement without relying on remote push services.

Notification Services307L

Ntfy

Implement ntfy.sh for simple HTTP-based pub/sub push notifications. Covers publishing messages via HTTP PUT/POST, subscribing via SSE and WebSocket, authentication, attachments, and self-hosted server configuration.

Notification Services209L

Pushbullet

Integrate Pushbullet to send real-time notifications, links, and files across all your devices and to other Pushbullet users. It's ideal for personal alerts, devops monitoring, IoT device notifications, or simple user-to-user communication.

Notification Services155L