Volver a las notas

Web Push con n8n: un aviso en el móvil cuando entra un formulario

24 de septiembre de 2026·
webpushpwan8nnextjs

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

Notificación push en el móvil con un aviso de formulario nuevo

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

Formulario, webhook de n8n, servidor con VAPID, notificación en el móvil

  1. 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-keys y se guardan como variables de entorno.
  2. Un service worker que recibe el mensaje y muestra la notificación, aunque la pestaña esté cerrada.
  3. La suscripción. El navegador, con permiso del usuario, devuelve un endpoint y dos claves. Eso se guarda en el servidor.
  4. 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.js devuelve la clave pública VAPID para que la página pueda suscribirse.
  • subscribe.js guarda la suscripción (un JSON en disco con un volumen persistente).
  • send.js recibe { 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 POST a https://tu-web/api/push/send.
  • Cabecera x-push-secret con 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-push lo hace por ti. Aun así, mejor no meter datos personales completos en la notificación, porque se ven en la pantalla de bloqueo.

Fuentes