Firebase es la forma más rápida de tener notificaciones push, pero tiene un precio que no siempre se ve: un SDK más en la app, otro intermediario con los datos de tus usuarios y otra cuenta que mantener. Si tu backend ya es Node.js, hablar directamente con los servidores de Apple es más sencillo de lo que parece.
En esta guía lo montamos de principio a fin: la clave .p8 en el portal de Apple, el código de la app que obtiene el token del dispositivo y el servidor en Node.js que envía las notificaciones, con los errores que te vas a encontrar y cómo resolverlos.
¿Por qué prescindir de Firebase y conectar directamente con APNs?
Firebase ofrece una capa de abstracción muy cómoda, pero introducir un tercero entre tus servidores y los de Apple conlleva ciertas desventajas que a menudo se pasan por alto:
- Menor latencia: Al prescindir del servidor intermedio de Google, el paquete viaja directamente de tu backend en Node.js a los servidores de APNs, reduciendo milisegundos valiosos en entregas de tiempo real.
- Privacidad y cumplimiento normativo (RGPD): Menos intermediarios significa menos terceros procesando los identificadores de tus usuarios (device tokens) y el contenido de las notificaciones.
- Control total sobre el ciclo de vida: Tienes visibilidad directa de los códigos de respuesta de Apple (como tokens caducados o errores de certificado), lo que facilita una depuración precisa.
- Ligereza y reducción de dependencias: Eliminas SDKs pesados en el cliente y dependencias de terceros en tu backend.
Requisitos previos en el Apple Developer Portal
Para que Apple autorice a tu servidor Node.js a enviar mensajes a sus dispositivos, necesitas generar una clave de autenticación basada en tokens (.p8). Este método es mucho más moderno y cómodo que los antiguos certificados SSL (.p12), ya que una sola clave nunca caduca y sirve para todas las aplicaciones asociadas a tu cuenta de desarrollador.
Sigue estos pasos en el portal de Apple:
- Ve a developer.apple.com e inicia sesión con tu cuenta de pago.
- Navega a Certificates, Identifiers & Profiles > Keys.
- Haz clic en el botón
+para crear una nueva clave. - Asigna un nombre (por ejemplo, Backend Push Key) y marca la casilla Apple Push Notifications service (APNs).
- Descarga el archivo generado con extensión
.p8. Atención: Apple solo te permite descargar este archivo una única vez; guárdalo en un lugar seguro. - Anota los siguientes datos que necesitarás más adelante:
- Key ID: Un identificador alfanumérico de 10 caracteres asignado a la clave.
- Team ID: Visible en la esquina superior derecha del portal de desarrollador.
- Bundle ID: El identificador único de tu aplicación iOS (ej.
com.tuempresa.tuapp).
Arquitectura de la conexión: El papel fundamental de HTTP/2
Desde hace varias versiones, la API moderna de APNs funciona exclusivamente sobre el protocolo HTTP/2. Esta arquitectura ofrece ventajas operativas enormes frente al obsoleto HTTP/1.1:
- Multiplexación: Permite enviar cientos de notificaciones en paralelo a través de una única conexión TCP persistente, sin necesidad de abrir y cerrar sockets continuamente.
- Respuestas inmediatas en JSON: Apple devuelve estados claros (
200 OK,400 BadDeviceToken,410 Unregistered, etc.) en tiempo real para cada notificación individual. - Compresión de encabezados (HPACK): Reduce drásticamente el consumo de ancho de banda en envíos masivos.
Afortunadamente, el entorno de ejecución de Node.js incluye soporte nativo para HTTP/2 a través del módulo interno http2, aunque en producción también es muy común apoyarse en librerías probadas como @parse/node-apn o crear clientes ligeros mediante la librería nativa combinada con JSON Web Tokens (JWT).
En la app iOS: obtener el device token
Antes de enviar nada desde el servidor, la app tiene que pedir permiso al usuario, registrarse en APNs y mandar su device token a tu backend. Activa la capacidad Push Notifications en el target y añade algo así a tu AppDelegate:
import UIKit
import UserNotifications
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
guard granted else { return }
DispatchQueue.main.async {
application.registerForRemoteNotifications()
}
}
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// Convierte el token binario a texto hexadecimal
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
print("Device token: \(token)")
// Envía el token a tu backend Node.js (por ejemplo, POST /api/devices)
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("No se pudo registrar en APNs: \(error.localizedDescription)")
}
}
En una app SwiftUI, conecta esta clase con @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate en tu estructura App. Guarda el token asociado al usuario en tu base de datos: es la dirección a la que el servidor enviará cada notificación.
Configuración del entorno en Node.js
Para este ejemplo, utilizaremos la librería comunitaria de alto rendimiento @parse/node-apn, la cual gestiona internamente la conexión multiplexada HTTP/2, la rotación de tokens criptográficos JWT y el mantenimiento del pool de conexiones persistentes con los servidores de Apple.
Instala la librería en tu proyecto:
npm install @parse/node-apn
Coloca tu archivo .p8 dentro de una carpeta segura de tu backend (o carga su contenido mediante variables de entorno) y prepara la configuración.
Implementación paso a paso: Creando el servicio de notificaciones
A continuación, crearemos un script modular para instanciar el proveedor de APNs y despachar mensajes a dispositivos específicos.
1. Inicializar la conexión con el proveedor
Crea un archivo llamado apnsService.js:
const apn = require('@parse/node-apn');
const path = require('path');
// Configuración del proveedor
const options = {
token: {
key: path.join(__dirname, 'AuthKey_XXXXXXXXXX.p8'), // Ruta a tu clave .p8
keyId: 'XXXXXXXXXX', // Tu Key ID de 10 caracteres
teamId: 'YYYYYYYYYY' // Tu Team ID de Apple
},
production: process.env.NODE_ENV === 'production' // false = Sandbox, true = Producción
};
const apnProvider = new apn.Provider(options);
module.exports = apnProvider;
Nota: Si estás probando tu aplicación durante el desarrollo compilando directamente desde Xcode, el valor de
productiondebe serfalsepara apuntar al entorno Sandbox de Apple. En producción (TestFlight o App Store), debe sertrue.
2. Construir la notificación y enviarla
Ahora crearemos la lógica para componer el payload de la notificación y enviarlo a un token de dispositivo (el que registra la app iOS mediante didRegisterForRemoteNotificationsWithDeviceToken):
const apn = require('@parse/node-apn');
const apnProvider = require('./apnsService');
async function sendPushNotification(deviceToken, alertData, customData = {}) {
// 1. Instanciar la notificación
const notification = new apn.Notification();
// Configuración estándar de la notificación
notification.expiry = Math.floor(Date.now() / 1000) + 3600; // Expira en 1 hora
notification.badge = alertData.badge || 1;
notification.sound = alertData.sound || 'default';
// Contenido visible para el usuario
notification.alert = {
title: alertData.title,
subtitle: alertData.subtitle,
body: alertData.body
};
// Identificador de tu app (Obligatorio)
notification.topic = 'com.tuempresa.tuapp';
// Tipo de push (obligatorio en iOS 13+ para notificaciones visibles)
notification.pushType = 'alert';
// Datos personalizados adicionales (Payload custom)
notification.payload = customData;
try {
// 2. Enviar la notificación a través de la conexión HTTP/2 existente
const result = await apnProvider.send(notification, deviceToken);
// 3. Procesar respuestas exitosas
if (result.sent.length > 0) {
console.log(`Notificación enviada con éxito a ${result.sent.length} dispositivos.`);
}
// 4. Gestionar errores devueltos por APNs
if (result.failed.length > 0) {
result.failed.forEach(failure => {
console.error(`Error al enviar a ${failure.device}:`, failure.response ? failure.response.reason : failure.error);
// Si el token es inválido o el usuario desinstaló la app
// Ojo: BadDeviceToken también salta si mezclas Sandbox y Producción
if (failure.response && failure.response.reason === 'Unregistered') {
// Lógica para marcar o eliminar el token en tu base de datos
eliminarTokenInvalido(failure.device);
}
});
}
} catch (error) {
console.error('Error crítico al contactar con APNs:', error);
}
}
function eliminarTokenInvalido(token) {
console.log(`[BD] Eliminando token obsoleto de la base de datos: ${token}`);
}
// Ejemplo de uso
const targetDeviceToken = 'a1b2c3d4e5f6...'; // Token hexadecimal recibido desde iOS (normalmente 64 caracteres)
sendPushNotification(targetDeviceToken, {
title: '¡Hola desde Node.js!',
body: 'Esta notificación viaja directo por HTTP/2 sin pasar por Firebase.',
badge: 2
}, {
chatId: 1042,
tipo: 'nuevo_mensaje'
});
Gestión de errores y mejores prácticas con APNs
Implementar la conexión directa te exige manejar ciertos aspectos de arquitectura para garantizar un rendimiento óptimo:
- Reutilización del Provider: Nunca instancies
new apn.Provider()dentro del ciclo de cada petición o función de envío. El cliente HTTP/2 debe mantenerse vivo para aprovechar el pooling y la multiplexación. Crea una instancia singleton y utilízala en toda tu aplicación. - Depuración de tokens huérfanos: Apple recomienda dejar de enviar notificaciones a los tokens que ya no son válidos. Si recibes
Unregistered(código 410), elimina el token de tu base de datos. ConBadDeviceTokenve con cuidado: también aparece cuando mandas un token de desarrollo al entorno de producción (o al revés), así que comprueba primero que el valor deproductiones el correcto antes de borrar tokens en masa. - Respeto a las prioridades: Utiliza la propiedad
notification.priority:- 10 (Inmediata): Despierta el dispositivo de inmediato (ideal para chats o llamadas).
- 5 (Ahorro de energía): Entrega el mensaje cuando el sistema operativo considere conveniente, preservando la batería del usuario.
- Cierre limpio (Graceful Shutdown): Al apagar tu servidor Node.js, llama a
apnProvider.shutdown()para cerrar ordenadamente las conexiones HTTP/2 activas.
Errores típicos que conviene evitar
- Mezclar Sandbox y Producción. Un token de una app compilada desde Xcode no funciona contra el servidor de producción, y al revés.
- Topic incorrecto. El
topicdebe ser exactamente el Bundle ID de la app (sin sufijos, salvo en Live Activities o VoIP). - Olvidar el pushType. Desde iOS 13, sin
apns-push-typecorrecto algunas notificaciones no se entregan. - Perder la clave .p8. Solo se descarga una vez. Guárdala en un gestor de secretos, no en el repositorio.
Conclusión
Si tu app solo es para iOS y ya tienes backend en Node.js, enviar las push directamente a APNs es una decisión muy razonable: menos dependencias y control total. Si también tienes app Android, Firebase sigue siendo cómodo porque unifica las dos plataformas.
Lo más importante es tener claro el entorno (Sandbox o Producción) de cada token: casi todos los «no me llegan las notificaciones» vienen de ahí.
¿Quieres este backend ya montado y probado?
En el iOS Backend Starter tienes todo lo de este artículo funcionando: cliente APNs por HTTP/2 con tu clave .p8, registro de dispositivos, limpieza automática de tokens caducados, Live Activities y un comando para probar tu clave contra tu iPhone en segundos. Incluye además login con Sign in with Apple, Stripe e IA con Ollama, y una app SwiftUI de ejemplo.
Artículos relacionados: Notificaciones Push en iOS: Guía definitiva para configurar certificados APNs · PHP 8.5 vs Node.js: Cuál elegir para el backend de tu app móvil · Servidores VPS baratos para Apps Móviles: Cómo desplegar tu backend PHP/Node sin morir en el intento