Notificaciones Push en React Native 2026: Expo Notifications, FCM, APNs y Deep Linking
Guía práctica para implementar notificaciones push en React Native con Expo, FCM v1 y APNs en 2026. Configuración, tokens, deep linking, background handlers y envío desde el servidor con código real.
Las notificaciones push en React Native se implementan en 2026 combinando expo-notifications con Firebase Cloud Messaging (FCM v1) para Android y Apple Push Notification service (APNs) para iOS, todo orquestado por EAS Build para gestionar credenciales. He enviado producción con este stack en seis apps y la conclusión es simple: si estás en el ecosistema Expo, esta es la ruta con menor fricción para llegar a un token en menos de un día, gestionar deep links de forma fiable y evitar el 90% de los footguns nativos. Este artículo cubre el flujo completo, con código que ejecutas tal cual.
expo-notifications abstrae FCM y APNs detrás de un solo token (ExpoPushToken), pero desde SDK 53 ya no funciona en Expo Go en Android: necesitas un development build.
Android en 2026 requiere FCM v1 con service account JSON (la API Legacy fue apagada por Google); iOS usa APNs con la AuthKey_XXXX.p8.
El setNotificationHandler se llama a nivel de módulo (fuera de cualquier componente) o las notificaciones en foreground quedarán silenciadas.
Para tareas en segundo plano usa Notifications.registerTaskAsync con expo-task-manager; en el flujo bare, usa messaging().setBackgroundMessageHandler de React Native Firebase.
Guarda ambos tokens en tu backend (Expo y nativo). Los proveedores de marketing (Braze, Customer.io) exigen tokens FCM/APNs directos.
Para deep linking desde una push, combina addNotificationResponseReceivedListener con Linking.openURL y usa getLastNotificationResponseAsync al arranque en frío.
Arquitectura del pipeline de push en 2026
La pila mental que uso al debuggear cualquier problema de push es esta cadena: tu servidor → servicio push (Expo/FCM/APNs) → el sistema operativo → tu app. En un flujo con Expo, el orden se convierte en: tu backend hace un POST a exp.host/--/api/v2/push/send, Expo empaqueta y reenvía a FCM v1 (Android) o APNs (iOS), y el sistema operativo del dispositivo decide finalmente si entregarla, mostrarla o descartarla. Este último paso es donde se pierde el 12-18% de entregas en Android por Doze/App Standby, y un 4-8% en iOS por Low Power Mode y Focus filters.
El diseño de Expo Notifications es lo que en su día hizo esto tolerable: un solo ExpoPushToken por dispositivo, portable entre iOS y Android, y una API común para permisos, canales, categorías y respuestas del usuario. La API de Expo firma con tus credenciales en EAS y responde con tickets asíncronos que luego consultas para saber si FCM/APNs aceptaron el mensaje. Es una capa muy fina, no una caja negra: puedes seguir enviando directamente a FCM y APNs con el device token nativo si tu stack de marketing lo requiere.
Mi regla de arquitectura para 2026: envía por Expo hasta que un requisito específico (analytics de proveedor, image attachments cifradas end-to-end, latencia sub-2s en push masivos) te fuerce a la ruta nativa. Cambiar más tarde es un fin de semana de trabajo, no una reescritura.
Instalación y configuración inicial en Expo
Empezamos desde una app Expo con New Architecture activada (default en SDK 52+). Instala las tres piezas base:
npx expo install expo-notifications expo-device expo-constants
npx expo install expo-task-manager # necesario si vas a usar background handlers
A continuación configura app.json con el plugin de expo-notifications y los permisos iOS. El plugin genera nativamente el entitlements de push y el background mode de remote-notification:
Una advertencia importante: desde SDK 53, expo-notifications ya no funciona para push remotas en Expo Go en Android. Necesitas un development build generado con eas build --profile development. Este ha sido, sin exagerar, el motivo #1 por el que la gente escribe issues diciendo "no me llega nada". Si nunca has generado uno, revisa la guía de despliegue con EAS Build, EAS Submit y EAS Update antes de continuar.
Configurar FCM v1 para Android
Google apagó la Legacy FCM API a mediados de 2024 y la sustituyó por FCM HTTP v1, que autentica con un JSON de service account en vez de una clave estática. Todos los tutoriales anteriores a 2025 que hablan de "Server Key" están obsoletos. El flujo actual es:
Crea o abre tu proyecto Firebase y registra una app Android con el mismo package que declaraste en app.json.
Descarga el google-services.json y colócalo en la raíz del proyecto. No lo commitees: usa EAS secrets.
En Firebase Console, ve a Configuración del proyecto → Cuentas de servicio → Firebase Admin SDK y genera una nueva clave privada. Guarda ese JSON.
Sube el JSON a EAS: eas credentials, elige Android, luego FCM V1 credentials, y pega el service account.
El google-services.json del cliente y el service account del servidor son cosas distintas, y ambas son necesarias. En Android además debes declarar un canal de notificación explícito antes de mostrar cualquier push, o el sistema simplemente la descarta:
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
export async function setupAndroidChannel() {
if (Platform.OS !== 'android') return;
await Notifications.setNotificationChannelAsync('default', {
name: 'General',
importance: Notifications.AndroidImportance.HIGH,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#0F172A',
sound: 'default',
});
}
Configurar APNs para iOS
iOS es más simple, porque Expo genera y sube la AuthKey por ti si se lo permites. Cuando corras eas build --platform ios, la CLI preguntará: "¿Quieres configurar Apple Push Notifications y generar un APNs key?". Responde sí a menos que ya administres claves manualmente. Bajo el capó, Expo:
Crea una AuthKey_XXXXXXXX.p8 en tu Apple Developer account (o reutiliza una existente).
Habilita el Push Notifications capability en el App ID.
Sube la clave a los servidores de Expo para que su servicio pueda firmar JWTs contra APNs.
Si prefieres administrarlo tú mismo (típico en equipos con SSO Apple o en compliance estricto), genera la .p8 en Apple Developer Portal, apunta el Key ID y el Team ID, y súbelos con eas credentials. Rota esas claves cada 12-18 meses. Si Apple detecta uso sospechoso las revoca sin aviso, y verás InvalidProviderToken en los tickets de Expo.
A nivel de código no hay nada específico de iOS que necesites hacer, ya que expo-notifications abstrae APNs entera. Solo asegúrate de que UIBackgroundModes incluya remote-notification (ya lo pusimos en app.json), o iOS no despertará tu app para procesar payloads silenciosos.
Cómo registrar el dispositivo y obtener el push token
Este es el "hola mundo" de push notifications. Escríbelo una vez, refactoriza en un hook, y no vuelvas a mirarlo:
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
// IMPORTANTE: al nivel de módulo, fuera de cualquier componente
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export async function registerForPushNotificationsAsync() {
if (!Device.isDevice) {
console.warn('Las push requieren un dispositivo físico');
return null;
}
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'General',
importance: Notifications.AndroidImportance.HIGH,
});
}
const { status: existing } = await Notifications.getPermissionsAsync();
let finalStatus = existing;
if (existing !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.warn('El usuario rechazó los permisos de push');
return null;
}
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) throw new Error('Falta el EAS projectId en app.json');
const tokenResponse = await Notifications.getExpoPushTokenAsync({ projectId });
return tokenResponse.data; // "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
}
Un par de decisiones deliberadas en ese código. Primero, setNotificationHandler vive fuera del componente porque debe ejecutarse antes de que llegue cualquier notificación. Si lo pones en un useEffect, una push que aterrice antes de que el efecto corra queda silenciada. Segundo, uso shouldShowBanner y shouldShowList (las claves nuevas en SDK 51+); shouldShowAlert sigue funcionando pero está deprecada.
Envía el token a tu backend inmediatamente después de obtenerlo, y también en cada arranque de la app: los tokens rotan cuando el usuario reinstala, restaura desde backup o borra datos. Un endpoint idempotente POST /api/push-tokens con { userId, expoPushToken, deviceToken, platform } es todo lo que necesitas.
Cómo manejar notificaciones push en segundo plano
Hay dos rutas según tu workflow. En Expo puro con expo-notifications, la API oficial es registerTaskAsync. En bare workflow con @react-native-firebase/messaging, es setBackgroundMessageHandler. Elige una y no mezcles (aprendí eso a las malas, cuando dos handlers competían por el mismo payload y ninguno terminaba la tarea).
Con expo-notifications y expo-task-manager
import * as TaskManager from 'expo-task-manager';
import * as Notifications from 'expo-notifications';
const BACKGROUND_TASK = 'BACKGROUND-NOTIFICATION-TASK';
TaskManager.defineTask(BACKGROUND_TASK, ({ data, error }) => {
if (error) {
console.error('Background task error', error);
return;
}
// data.notification.request.content.data trae tu payload custom
const payload = (data as any)?.notification?.request?.content?.data;
if (payload?.type === 'sync') {
// Ejecuta sincronización silenciosa aquí
}
});
Notifications.registerTaskAsync(BACKGROUND_TASK);
Un detalle crítico: el console.log desde tareas en background no es fiable en iOS. Si necesitas depurar, escribe a AsyncStorage o MMKV con un timestamp y revisa después. Para persistir estado local puedes echar mano de las bases de datos locales que ya cubrimos.
Este handler corre incluso con la app killed, siempre que la push contenga content-available: 1 (iOS) o priority: high con solo data (Android). Si la push tiene notification además de data, el sistema la muestra directamente y tu handler puede no ejecutarse. Honestamente, este es el motivo #1 de "mi background handler no corre en producción" en foros y GitHub issues, y me lo he tragado yo mismo tres veces en apps distintas.
Cómo hacer deep linking desde una notificación push
El deep link desde push tiene tres estados diferentes que debes manejar por separado: app abierta, app en background, y app killed. El error clásico es asumir que el mismo listener cubre los tres, cuando en realidad getLastNotificationResponseAsync es el que rescata la respuesta cuando el usuario mató la app y el tap la despierta desde cero.
import { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
import { router } from 'expo-router';
export function useNotificationDeepLinks() {
useEffect(() => {
// Caso 1: app en foreground o background, listener en vivo
const sub = Notifications.addNotificationResponseReceivedListener((response) => {
handleTap(response.notification.request.content.data);
});
// Caso 2: app killed y despertada por el tap
Notifications.getLastNotificationResponseAsync().then((response) => {
if (response) handleTap(response.notification.request.content.data);
});
return () => sub.remove();
}, []);
}
function handleTap(data: any) {
if (data?.screen === 'order' && data?.orderId) {
router.push(`/orders/${data.orderId}`);
} else if (data?.url) {
router.push(data.url);
}
}
El router es de Expo Router. Si aún estás en React Navigation clásico, usa navigationRef.navigate(...) con el mismo ref que expones a nivel de raíz. Cubrimos ambos patrones en detalle en la guía de navegación con Expo Router y React Navigation 7, incluyendo la configuración de linking.prefixes que necesitas para que las URLs universales funcionen.
En el payload que envías desde el servidor, incluye siempre un objeto data con las claves que tu handler espera. Ejemplo:
{
"to": "ExponentPushToken[...]",
"title": "Pedido enviado",
"body": "Tu paquete está en camino",
"data": { "screen": "order", "orderId": "42" }
}
Enviar notificaciones desde tu servidor (Expo, FCM, APNs)
Tres opciones, y eliges según tu volumen y necesidades. Para la mayoría de apps con menos de 100k usuarios, la API de Expo es más que suficiente y evita firmar JWTs a mano. Aquí un ejemplo mínimo en Node con backoff y batching:
import { Expo, ExpoPushMessage } from 'expo-server-sdk';
const expo = new Expo({ accessToken: process.env.EXPO_ACCESS_TOKEN });
export async function sendPush(messages: ExpoPushMessage[]) {
const chunks = expo.chunkPushNotifications(messages);
const tickets = [];
for (const chunk of chunks) {
try {
const receipt = await expo.sendPushNotificationsAsync(chunk);
tickets.push(...receipt);
} catch (err) {
console.error('Expo push error', err);
}
}
// Persiste tickets para reconciliar receipts luego (24-48h después)
return tickets;
}
Después de enviar recibirás tickets, no confirmaciones de entrega. A los 15 minutos consulta getPushNotificationReceiptsAsync para saber si FCM/APNs aceptaron cada mensaje. Los errores comunes son DeviceNotRegistered (borra el token de tu DB), MessageTooBig (payload de más de 4KB), y MessageRateExceeded (backoff exponencial).
Si necesitas ir directo a FCM v1 sin pasar por Expo (típicamente por analytics de proveedor o entrega sub-segundo), la ruta es hacer POST a https://fcm.googleapis.com/v1/projects/YOUR_PROJECT/messages:send con un JWT firmado por tu service account. Los detalles del payload y la firma están en la referencia oficial de FCM HTTP v1. Y si prefieres el camino documentado por Expo para pasar credenciales custom, Expo mantiene una guía específica de sending con FCM y APNs que vale la pena tener a mano. Para casos límite en iOS, la documentación oficial de UserNotifications de Apple aclara qué payloads despiertan cada modo.
Por qué mis notificaciones push no llegan en Android
He debuggeado esto suficientes veces para tener una checklist mental que funciona el 95% del tiempo. Si Android no recibe pushes, revisa en orden:
Credenciales FCM v1. Corre eas credentials, selecciona Android, y verifica que el service account JSON está subido y no ha expirado. Si dice "Legacy Server Key", ese es tu problema, porque Google apagó ese endpoint.
Package name. El package en app.json debe coincidir exactamente con el registrado en Firebase (incluyendo mayúsculas). Un typo silencioso rompe todo.
Development build vs Expo Go. Si estás probando en Expo Go en Android, no va a funcionar. Genera un dev build.
Canal de notificación. En Android 8+, una push sin canal registrado se descarta silenciosamente. Verifica que setNotificationChannelAsync('default', ...) corrió antes de la primera push.
Google Play Services. En emuladores sin GApps la push nunca llega. Usa un emulador con Google APIs.
Battery optimization del OEM. Xiaomi, OnePlus, Huawei. Pide al usuario ir a Settings → Battery → App startup manager y whitelistear tu app.
Priority. Envía con priority: 'high' desde el servidor para atravesar Doze. Bajo normal, Android puede diferir la entrega horas.
iOS suele ser más determinista: si el ticket de Expo vuelve ok y el device tiene conectividad, la push llega. Cuando no, es 90% de las veces InvalidProviderToken (rota la .p8) o Focus/Do Not Disturb del usuario.
Alternativas: Notifee, OneSignal y React Native Firebase
No todo el mundo tiene que quedarse en expo-notifications, y aquí saco mi opinión de lead: elige por caso de uso, no por moda.
Feature
expo-notifications
Notifee
OneSignal
RN Firebase
Instalación en Expo
Nativa
Plugin config
Plugin config
Plugin config
Local notifications
Básicas
Muy ricas (categorías, full-screen, grupos)
Vía SDK
Básicas
Push remotas iOS/Android
Sí (Expo push)
No (necesita FCM/APNs)
Sí (dashboard SaaS)
Sí (FCM directo)
Segmentación y A/B
No
No
Sí
Manual
Analytics de entrega
Tickets/receipts
Ninguno
Dashboard completo
FCM Console
Coste
Gratis
Gratis
Freemium (paga a partir de ~10k users)
Gratis
Curva de aprendizaje
Baja
Media
Baja
Media-alta
Mi decisión rápida: Expo Notifications por defecto. Notifee si necesitas notificaciones locales sofisticadas (recordatorios de medicación, alertas de calendario con categorías interactivas). OneSignal cuando marketing va a manejar los envíos desde un dashboard y necesitan segmentación sin depender de ingeniería. React Native Firebase solo si ya estás profundamente en el ecosistema Firebase (Firestore, Auth, Remote Config) y prefieres unificar la stack. Puedes complementar cualquiera de estas con integraciones más amplias siguiendo los patrones de módulos nativos con Expo Modules API.
Preguntas frecuentes
¿Puedo usar Expo Go para probar notificaciones push en 2026?
No. Desde el SDK 53, expo-notifications ya no soporta push remotas en Expo Go en Android, y las restricciones en iOS son similares. Necesitas un development build generado con eas build --profile development. Las notificaciones locales sí siguen funcionando en Expo Go, pero cualquier prueba realista de FCM/APNs requiere un dev build en dispositivo físico.
¿Cuál es la diferencia entre ExpoPushToken y el device push token?
El ExpoPushToken es una abstracción que apunta al servicio push de Expo, que a su vez reenvía a FCM/APNs. El device push token es el token nativo FCM (Android) o APNs (iOS) que puedes usar directamente sin pasar por Expo. Guarda ambos: el primero para enviar desde tu backend con la SDK de Expo, el segundo si un día integras Braze, Customer.io u otro proveedor que requiera acceso directo a FCM/APNs.
¿Cómo envío una notificación push a todos los usuarios de mi app?
Recoge todos los ExpoPushToken de tu base de datos, divídelos en chunks de 100 con Expo.chunkPushNotifications(), y envía cada chunk vía sendPushNotificationsAsync. La API de Expo tolera hasta 600 mensajes por segundo por token de acceso. Para volúmenes mayores, considera usar OneSignal o enviar directamente a FCM v1 con un pool de workers.
¿Por qué no se muestra la notificación cuando la app está abierta?
Porque no llamaste a Notifications.setNotificationHandler, o lo llamaste dentro de un useEffect en vez de a nivel de módulo. Por defecto, iOS y Android silencian las notificaciones cuando la app está en primer plano; el handler es el que le dice al sistema que muestre banner, sonido y badge. Ponlo fuera de cualquier componente para que corra antes de que llegue la primera push.
¿Las notificaciones push tienen coste en 2026?
Enviar por Expo, FCM directo o APNs directo es gratis, sin límites duros para la mayoría de casos de uso. Los costes aparecen cuando usas plataformas SaaS como OneSignal (freemium hasta ~10k usuarios) o Braze (enterprise), o si construyes analytics propios que consumen infraestructura (colas, workers, DB de tokens). Para una app típica con menos de un millón de usuarios activos, el stack Expo + FCM + APNs es gratis, fin.