Nota revisada y reescrita el 18 de octubre de 2026: la versión original tenía errores y datos sin fuente.

Un aviso en el móvil a los pocos segundos de que alguien rellene un formulario, sin app en ninguna tienda ni servicio de notificaciones de pago: basta con la Web Push API del navegador, un service worker y un nodo HTTP en n8n. Aquí va cómo está montado en richi.codes, con su código.
Las piezas

- Claves VAPID. Un par de claves (pública y privada) que identifican a tu servidor ante el servicio de push del navegador (el de Google, Mozilla o Apple). Se generan una vez con
npx web-push generate-vapid-keysy se guardan como variables de entorno. - Un service worker que recibe el mensaje y muestra la notificación, aunque la pestaña esté cerrada.
- La suscripción. El navegador, con permiso del usuario, devuelve un
endpointy dos claves. Eso se guarda en el servidor. - El envío. El servidor firma el mensaje con la clave privada y lo manda al
endpoint. Aquí lo dispara n8n cuando llega un formulario.
El service worker
Lo esencial de public/sw-notis.js:
self.addEventListener('push', (event) => {
let data = {};
try {
data = event.data ? event.data.json() : {};
} catch (e) {
data = { body: event.data && event.data.text() };
}
event.waitUntil(
self.registration.showNotification(data.title || 'richi.codes', {
body: data.body || '',
icon: '/android-icon-192x192.png',
data: { url: data.url || '/' },
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(self.clients.openWindow(event.notification.data.url));
});
El event.waitUntil importa: sin él, el navegador puede matar el service worker antes de que la notificación se muestre.
Suscribirse desde la página
Hay que pedir permiso como respuesta a un clic, no al cargar la página: los navegadores penalizan o bloquean las peticiones de permiso que no vienen de un gesto del usuario.
async function subscribe(publicKey) {
const permission = await Notification.requestPermission();
if (permission !== 'granted') return null;
const reg = await navigator.serviceWorker.register('/sw-notis.js');
await navigator.serviceWorker.ready;
return (
(await reg.pushManager.getSubscription()) ||
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey),
})
);
}
userVisibleOnly: true es obligatorio en Chrome: te comprometes a mostrar una notificación por cada push. urlBase64ToUint8Array es la conversión habitual de la clave pública VAPID (base64 URL-safe) a bytes.
El servidor: tres rutas
En pages/api/push/ hay tres rutas pequeñas:
key.jsdevuelve la clave pública VAPID para que la página pueda suscribirse.subscribe.jsguarda la suscripción (un JSON en disco con un volumen persistente).send.jsrecibe{ title, body, url }y la envía a todas las suscripciones guardadas.
subscribe y send exigen una cabecera x-push-secret: sin ella, cualquiera podría mandarte notificaciones. El envío usa la librería web-push:
import webpush from 'web-push';
webpush.setVapidDetails(
'mailto:hola@richi.codes',
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
);
for (const sub of subscriptions) {
try {
await webpush.sendNotification(sub, JSON.stringify({ title, body, url }));
} catch (err) {
// 404 o 410: la suscripción ya no existe, se borra de la lista
if (err.statusCode === 404 || err.statusCode === 410) remove(sub);
}
}
Limpiar las suscripciones que devuelven 404 o 410 evita que la lista crezca con navegadores que ya se desinstalaron o revocaron el permiso.
El lado de n8n
El workflow del formulario ya existía (manda un email). Solo hay que añadirle en paralelo un nodo HTTP Request:
- Método
POSTahttps://tu-web/api/push/send. - Cabecera
x-push-secretcon el secreto (guardado como credencial, no escrito en el nodo). - Cuerpo JSON:
{ "title": "Formulario nuevo", "body": "{{ $json.nombre }}: {{ $json.mensaje.slice(0, 80) }}", "url": "/" }.
El mismo endpoint sirve para cualquier otro evento que n8n vea: un workflow que falla, una web que deja de responder o un pago en modo test.
Lo que hay que tener en cuenta
- iPhone y iPad: desde iOS/iPadOS 16.4, Safari solo permite Web Push si la web está añadida a la pantalla de inicio y tiene un manifest con
display: standalone. En una pestaña normal de Safari no aparece la opción. - HTTPS obligatorio, salvo en
localhost. - El permiso es de un solo intento: si el usuario lo deniega, la web no puede volver a preguntar. Solo se puede cambiar desde los ajustes del navegador.
- Las suscripciones caducan. Por eso el paso de borrar las que devuelven 404 o 410.
- El payload va cifrado de extremo a extremo (RFC 8291), y
web-pushlo hace por ti. Aun así, mejor no meter datos personales completos en la notificación, porque se ven en la pantalla de bloqueo.