Saltar a contenido

Capítulo 28 — Triggers de Cloud Functions: Automatización basada en eventos

Recursos visuales propuestos

Antes de desarrollar este capítulo conviene diferenciar entre recursos que introducen la idea de evento y recursos que representan la arquitectura real del sistema. Las explicaciones sobre qué es un evento, la comparación entre proceso manual y automatizado, los ejemplos de automatización, los casos de uso y el flujo básico de un trigger deben resolverse como imágenes didácticas, porque su objetivo es construir intuición de forma progresiva. En cambio, la arquitectura orientada a eventos, el flujo Firestore → Trigger → Cloud Function → Firestore, la integración entre Authentication, Storage y Functions, y la arquitectura completa de automatización del proyecto del libro deben representarse como diagramas SVG, ya que incluyen múltiples componentes, rutas de ejecución, relaciones causales y responsabilidades distribuidas entre servicios.[cite:1][cite:2]

Imágenes didácticas

  1. ¿Qué es un evento? Conviene como imagen didáctica porque el objetivo es fijar el concepto de causa y reacción.
  2. Comparación entre proceso manual y automatizado. Debe ser imagen didáctica porque ilustra cambio de modelo operativo, no topología técnica.
  3. Ejemplos de automatización. Conviene como imagen didáctica porque agrupa escenarios frecuentes y facilita recordación.
  4. Casos de uso. Debe ser imagen didáctica porque ayuda a clasificar problemas reales que se benefician del enfoque orientado a eventos.
  5. Flujo básico de un Trigger. Es mejor como imagen didáctica porque resume la secuencia evento → función → efecto sin detallar todavía infraestructura.

Diagramas SVG

  1. Arquitectura Event-Driven. Debe ser SVG porque necesita mostrar servicios emisores, enrutamiento de eventos, funciones y efectos posteriores.
  2. Flujo Firestore → Trigger → Cloud Function → Firestore. Debe ser SVG porque representa una secuencia técnica con posibilidad de retroalimentación y riesgo de recursividad.[cite:1]
  3. Integración entre Authentication, Storage y Functions. Debe ser SVG porque intervienen fuentes de eventos distintas y salidas automatizadas heterogéneas.[cite:1][cite:2]
  4. Arquitectura completa de automatización del proyecto del libro. Debe ser SVG porque sintetiza varias automatizaciones coordinadas dentro de una misma solución distribuida.

Objetivos de aprendizaje

Al finalizar este capítulo, el lector será capaz de:

  • Comprender qué es un trigger y cómo encaja dentro de una arquitectura orientada a eventos.
  • Identificar los principales triggers de Cloud Functions v2 para Firestore y Cloud Storage, así como el papel de Eventarc y Pub/Sub en automatizaciones avanzadas.[cite:1][cite:2]
  • Entender qué ocurre internamente cuando se dispara un evento y cómo viaja hasta la función serverless.
  • Diseñar automatizaciones seguras, idempotentes y eficientes.
  • Evitar bucles infinitos, ejecuciones duplicadas e impactos innecesarios en costos y rendimiento.[cite:1]
  • Integrar procesos automáticos reales dentro del proyecto oficial del libro, una plataforma educativa con credenciales digitales.

Introducción

Hasta ahora el lector ya sabe construir backend explícito con HTTP Functions y Callable Functions. En ambos casos, la aplicación o un cliente externo llama una función de manera directa. Ese modelo resuelve muy bien las operaciones bajo demanda. Sin embargo, muchas aplicaciones modernas necesitan otra capacidad: reaccionar automáticamente cuando ocurre algo importante.

Ahí es donde entran los triggers. Un trigger permite ejecutar lógica sin que el usuario pulse un botón ni llame explícitamente a un endpoint. La aplicación no “ordena” que suceda el proceso; el sistema lo dispara automáticamente al detectar un evento relevante, como la creación de un documento en Firestore, la carga de una imagen en Storage o la publicación de un mensaje en un sistema de eventos.[cite:1][cite:2]

Esta forma de trabajar cambia profundamente la arquitectura. En lugar de construir aplicaciones que dependen de procesos manuales o de secuencias imperativas desde el cliente, se diseñan sistemas reactivos. El frontend se concentra en capturar acciones y mostrar información; la plataforma observa cambios y ejecuta tareas derivadas por sí misma.

En una plataforma educativa como la del proyecto transversal del libro, esto resulta especialmente poderoso. Cuando un usuario se registra, se puede crear su perfil automáticamente. Cuando sube una fotografía, se puede generar una miniatura y validar el formato. Cuando se actualiza una inscripción, se pueden recalcular estadísticas y registrar auditoría. Cuando aparece un nuevo archivo oficial, se puede iniciar una cadena de procesos documentales. Nada de esto tiene por qué depender del navegador del usuario.

Este capítulo enseña precisamente esa capa de automatización. Se explicará qué es la arquitectura orientada a eventos, cómo funcionan los triggers de Cloud Functions v2, qué diferencia existe entre reaccionar a Firestore, Storage o un bus de eventos y qué prácticas deben aplicarse para que la automatización no se convierta en una fuente de recursividad, costos inesperados o comportamiento impredecible.

Desarrollo completo

Introducción al modelo basado en eventos

¿Qué es un Trigger?

Un trigger es una condición de activación asociada a un evento. Cuando ese evento ocurre, la plataforma ejecuta una función específica. En Firebase, un trigger permite responder automáticamente a cambios generados por servicios como Firestore o Cloud Storage, sin necesidad de modificar el cliente que originó el cambio.[cite:1][cite:2]

La documentación de Firestore lo expresa de forma directa: con Cloud Functions se pueden manejar eventos de Firestore sin necesidad de actualizar el código cliente. La función espera cambios en un documento o conjunto de documentos, se dispara cuando ocurre un evento y recibe datos relacionados con ese cambio, incluyendo el estado anterior y posterior cuando corresponde.[cite:1]

Arquitectura orientada a eventos

La arquitectura orientada a eventos, o Event-Driven Architecture, parte de una idea sencilla: los componentes del sistema se comunican mediante hechos relevantes. No se trata solo de llamar procedimientos remotos, sino de reaccionar a cambios de estado.

En lugar de pensar únicamente en “el cliente hace una petición y el servidor responde”, se piensa en términos como:

  • se creó un documento;
  • se modificó un registro;
  • se eliminó un archivo;
  • llegó un mensaje al bus de eventos;
  • se completó una operación en un servicio externo.

Cada uno de esos hechos puede convertirse en una señal para que otro componente actúe. El resultado es un sistema más desacoplado y más capaz de automatizar procesos complejos.

Ventajas frente a procesos manuales

Frente a los procesos manuales o controlados desde el frontend, el enfoque por eventos tiene ventajas claras:

  • reduce dependencia del cliente;
  • centraliza lógica sensible en backend;
  • permite reaccionar incluso si el usuario ya cerró la app;
  • mejora consistencia operativa;
  • facilita encadenar tareas derivadas;
  • favorece arquitecturas más modulares y escalables.

Además, al ejecutarse en infraestructura administrada y elástica, los triggers encajan bien con la naturaleza serverless de Cloud Functions.[cite:1][cite:2]

Casos de uso

Los casos más frecuentes son:

  • crear perfiles automáticamente cuando aparece un nuevo usuario o documento base;
  • procesar imágenes al cargarse un archivo en Storage;
  • generar derivados documentales;
  • enviar notificaciones automáticas;
  • recalcular estadísticas o proyecciones;
  • registrar auditoría;
  • sincronizar información entre colecciones;
  • reaccionar a eventos internos del sistema o a eventos publicados en un bus.

Firestore Triggers

Firestore es una de las fuentes de eventos más importantes en el ecosistema Firebase. Esto se debe a que muchas aplicaciones usan Firestore como fuente principal de estado de negocio. Cuando cambia un documento, se puede interpretar que ocurrió un hecho relevante en el sistema.

onDocumentCreated

onDocumentCreated se activa cuando un documento se escribe por primera vez.[cite:1] Es ideal para procesos iniciales, por ejemplo:

  • crear registros derivados;
  • generar auditoría de alta;
  • inicializar estadísticas;
  • lanzar automatizaciones posteriores.

Ejemplo conceptual para el proyecto del libro: cuando se crea un documento en solicitudesCredencial/{id}, se puede iniciar la generación de credencial digital.

import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { getFirestore } from "firebase-admin/firestore";
import { initializeApp } from "firebase-admin/app";

initializeApp();
const db = getFirestore();

export const inicializarSolicitudCredencial = onDocumentCreated(
  "solicitudesCredencial/{solicitudId}",
  async (event) => {
    const snapshot = event.data;
    if (!snapshot) return;

    const data = snapshot.data();

    await db.collection("auditoria").add({
      tipo: "SOLICITUD_CREDENCIAL_CREADA",
      solicitudId: event.params.solicitudId,
      userId: data.userId,
      createdAt: new Date().toISOString(),
    });
  }
);

onDocumentUpdated

onDocumentUpdated se activa cuando un documento ya existente cambia en alguno de sus valores.[cite:1] Este trigger es especialmente útil cuando el proceso depende de detectar transiciones de estado.

Ejemplos:

  • cuando estatus cambia de pendiente a aprobado;
  • cuando fotoProcesada pasa de false a true;
  • cuando un docente modifica una evaluación y deben recalcularse estadísticas.

onDocumentDeleted

onDocumentDeleted se activa cuando un documento se elimina.[cite:1] Aunque muchos sistemas modernos prefieren eliminación lógica, este trigger sigue siendo útil para:

  • liberar recursos derivados;
  • escribir auditoría final;
  • limpiar colecciones espejo;
  • desindexar elementos auxiliares.

onDocumentWritten

onDocumentWritten cubre creación, actualización o eliminación.[cite:1] Es cómodo cuando importa reaccionar a cualquier cambio y luego decidir internamente qué tipo de transición ocurrió.

Sin embargo, conviene usarlo con criterio. Si el proceso solo aplica a una actualización, onDocumentUpdated comunica mejor la intención y reduce complejidad cognitiva.

Acceso al documento antes y después del cambio

Una de las ventajas más importantes de los triggers de Firestore es que, para eventos de escritura o actualización, el evento contiene el estado antes y después del cambio. La documentación explica que event.data.before representa el snapshot previo y event.data.after el posterior.[cite:1]

Eso permite responder preguntas fundamentales como:

  • ¿qué campo cambió?
  • ¿el cambio fue semánticamente relevante?
  • ¿se produjo una transición de estado o un simple ajuste secundario?
  • ¿hay que ejecutar lógica derivada o salir inmediatamente?

Ejemplo:

import { onDocumentUpdated } from "firebase-functions/v2/firestore";

export const recalcularEstadoAlumno = onDocumentUpdated(
  "inscripciones/{inscripcionId}",
  async (event) => {
    const before = event.data.before.data();
    const after = event.data.after.data();

    if (before.estatus === after.estatus) {
      return;
    }

    if (after.estatus === "aprobado") {
      // Ejecutar automatización derivada
    }
  }
);

Detección de modificaciones

Detectar modificaciones no significa comparar todo el documento siempre. A nivel profesional, conviene detectar solo los campos de negocio que verdaderamente importan. La documentación muestra un ejemplo clásico: si el nombre no cambió, se sale de la función para evitar trabajo innecesario e impedir loops.[cite:1]

Esta idea es crucial. Una buena automatización no reacciona a cualquier diferencia irrelevante; reacciona solo a cambios significativos.

Prevención de bucles infinitos

La documentación oficial advierte explícitamente que cualquier vez que se escribe de nuevo sobre el mismo documento que disparó la función existe riesgo de generar un bucle infinito, y por eso debe garantizarse una salida segura cuando no sea necesario escribir nada.[cite:1]

Este es uno de los riesgos más importantes del capítulo. Por ejemplo:

  1. Un documento se actualiza.
  2. El trigger se dispara.
  3. La función vuelve a actualizar el mismo documento.
  4. Esa actualización vuelve a disparar la misma función.
  5. El ciclo continúa.

Para evitarlo, se aplican varias estrategias:

  • comparar before y after y salir si no hubo cambio relevante;[cite:1]
  • escribir en otro documento o colección en lugar del mismo;
  • usar flags como processed, thumbnailGenerated o statsUpdated;
  • diseñar transformaciones idempotentes;
  • separar fases del proceso.

Authentication Triggers

Aquí aparece una cuestión importante de actualización tecnológica. La petición del capítulo exige trabajar exclusivamente con Cloud Functions v2. Sin embargo, la documentación oficial más clara y estable para onUserCreated y onUserDeleted sigue apareciendo históricamente en la guía de Authentication triggers de primera generación, mientras que la evolución de segunda generación ha priorizado Eventarc y otros triggers; incluso el blog oficial de Firebase señaló que seguían trabajando en soportar los eventos onCreate y onDeleted de Authentication junto con otros eventos heredados.[cite:3][cite:4]

Desde una perspectiva profesional, esto debe explicarse con honestidad editorial: conceptualmente estos triggers son esenciales dentro de una arquitectura orientada a eventos, pero el nivel de soporte y paridad en v2 ha evolucionado por etapas. Por tanto, el lector debe distinguir entre el patrón arquitectónico y la disponibilidad concreta del trigger en una fecha dada.[cite:3][cite:4]

onUserCreated

Conceptualmente, onUserCreated responde al alta de un usuario en Firebase Authentication. La documentación de Authentication triggers explica que un evento de creación ocurre, por ejemplo, cuando un usuario crea una cuenta con email y contraseña, inicia sesión por primera vez con un proveedor federado, el desarrollador crea la cuenta mediante Admin SDK o se genera una nueva sesión anónima por primera vez; no se dispara cuando el primer acceso se realiza con custom token.[cite:4]

Arquitectónicamente, este evento es ideal para:

  • crear perfil inicial;
  • asignar configuración base;
  • enviar correo de bienvenida;
  • registrar auditoría de alta;
  • inicializar datos mínimos del usuario.

onUserDeleted

onUserDeleted se usa para reaccionar a la eliminación de una cuenta de Authentication.[cite:4] Resulta útil para:

  • borrar o anonimizar datos auxiliares;
  • cerrar accesos derivados;
  • registrar auditoría;
  • iniciar procesos de cumplimiento o retención.

Automatización del registro de usuarios

En el proyecto del libro, cuando aparece un nuevo usuario autenticado, tiene sentido crear automáticamente:

  • perfil en Firestore;
  • configuración inicial;
  • registro de actividad;
  • solicitud de credencial, si el rol del sistema lo requiere;
  • tarea de envío de correo de bienvenida.

Creación automática de perfiles

Este es uno de los mejores ejemplos para enseñar por qué un trigger debe vivir en backend y no en cliente. Si la app intentara crear el perfil solo desde el frontend, podrían aparecer problemas como:

  • el usuario cierra la aplicación antes de terminar;
  • una versión antigua del frontend no ejecuta el proceso;
  • diferentes clientes implementan la lógica de forma desigual;
  • un actor malicioso omite la creación del perfil;
  • se pierde consistencia entre Authentication y Firestore.

Un proceso backend reactivo garantiza mayor confiabilidad.

Storage Triggers

Cloud Storage es otra fuente crítica de eventos. La documentación oficial indica que se puede disparar una función en respuesta a carga, actualización o eliminación de archivos y carpetas en Cloud Storage.[cite:2]

onObjectFinalized

onObjectFinalized se activa cuando un nuevo objeto, o una nueva generación de uno existente, se crea correctamente en el bucket. Incluye copias y reescrituras, pero no cargas fallidas.[cite:2]

Es uno de los triggers más útiles de todo Firebase porque permite reaccionar a archivos recién subidos.

onObjectDeleted

onObjectDeleted se activa cuando un objeto se elimina permanentemente.[cite:2] Es útil para limpieza de derivados, borrado de miniaturas, sincronización con bases de datos o auditoría documental.

Procesamiento automático de imágenes

La propia guía oficial de Storage incluye un ejemplo de procesamiento de imágenes usando sharp para generar miniaturas automáticamente al subir un archivo.[cite:2] Este patrón es perfecto para el proyecto del libro cuando los usuarios suben fotografías para credenciales digitales.

Generación de miniaturas

El ejemplo oficial muestra dos salidas tempranas muy valiosas:

  • si el archivo no es imagen, la función termina;
  • si el archivo ya es una miniatura, la función termina para evitar procesarse a sí misma.[cite:2]

Ese diseño es una demostración práctica de prevención de recursividad y ahorro de costos.

Ejemplo adaptado:

import { onObjectFinalized } from "firebase-functions/v2/storage";
import { initializeApp } from "firebase-admin/app";
import { getStorage } from "firebase-admin/storage";
import * as logger from "firebase-functions/logger";
import path from "node:path";
import sharp from "sharp";

initializeApp();

export const generarMiniaturaPerfil = onObjectFinalized(async (event) => {
  const bucketName = event.data.bucket;
  const filePath = event.data.name;
  const contentType = event.data.contentType;

  if (!filePath || !contentType?.startsWith("image/")) {
    logger.info("El archivo no es una imagen procesable");
    return;
  }

  const fileName = path.basename(filePath);
  if (fileName.startsWith("thumb_")) {
    logger.info("La miniatura ya existe o el archivo ya es derivado");
    return;
  }

  const bucket = getStorage().bucket(bucketName);
  const [imageBuffer] = await bucket.file(filePath).download();
  const thumbnailBuffer = await sharp(imageBuffer)
    .resize({ width: 256, height: 256, withoutEnlargement: true })
    .webp({ quality: 82 })
    .toBuffer();

  const thumbFilePath = path.join(path.dirname(filePath), `thumb_${path.parse(fileName).name}.webp`);

  await bucket.file(thumbFilePath).save(thumbnailBuffer, {
    metadata: { contentType: "image/webp" },
  });
});

Conversión de formatos

La conversión de formatos es una extensión natural del procesamiento de imágenes. Por ejemplo:

  • JPEG a WebP para optimizar peso;
  • PNG a miniatura comprimida;
  • normalización de tamaño para credenciales;
  • generación de versiones de baja resolución para listados.

La clave arquitectónica es que esta lógica debe ocurrir en backend porque requiere consistencia, control de formatos y, en muchos casos, uso de librerías que no conviene ejecutar en cliente.

Pub/Sub

Introducción a Pub/Sub

Pub/Sub es un sistema de mensajería basado en publicación y suscripción. En lugar de que un componente invoque directamente a otro, publica un mensaje en un tópico. Otros componentes suscritos reaccionan a ese mensaje.

Dentro del ecosistema de Cloud Functions v2, Pub/Sub es relevante porque permite desacoplar procesos y construir canalizaciones más escalables de automatización. Además, la documentación de comparación de versiones destaca que v2 puede activarse mediante Eventarc y Pub/Sub como parte de su nueva infraestructura.[cite:3]

Publicación de eventos

Publicar un evento significa emitir un mensaje declarando que algo ocurrió. Ejemplos:

  • credencial.generada;
  • usuario.perfil.creado;
  • foto.procesada;
  • estadisticas.recalcular.

Suscripción

Suscribirse significa escuchar ese tipo de mensajes y reaccionar a ellos. Esta separación mejora la modularidad. Un mismo evento puede ser consumido por múltiples procesos sin que el emisor conozca todos los detalles.

Casos de uso

Pub/Sub resulta especialmente útil cuando:

  • un proceso debe desencadenar varios procesos secundarios;
  • se quieren desacoplar fases de automatización;
  • se necesita absorber picos sin acoplar operaciones en línea;
  • se preparan integraciones más cercanas a arquitecturas distribuidas.

Eventarc

¿Qué es Eventarc?

Eventarc es la capa de enrutamiento y entrega de eventos moderna del ecosistema Google Cloud que amplía la variedad de fuentes de eventos disponibles para Cloud Functions v2. El blog oficial de Firebase destaca precisamente que la segunda generación añade soporte para nuevos triggers impulsados por Eventarc y también soporte para Custom Events.[cite:3]

Integración con Cloud Functions v2

La importancia de Eventarc en v2 es estructural. No es un complemento accidental, sino parte de la evolución del modelo de funciones basado en Cloud Run y orientado a eventos más amplios. Gracias a ello, las funciones de segunda generación pueden reaccionar no solo a los triggers clásicos, sino también a más tipos de eventos del ecosistema cloud.[cite:3]

Eventos personalizados

El soporte a eventos personalizados abre una puerta arquitectónica muy poderosa. Permite emitir señales propias del dominio de negocio, no solo eventos generados automáticamente por Firebase o Google Cloud. En un sistema maduro, esto facilita flujos como:

  • credencial.aprobada;
  • expediente.completo;
  • reporte.mensual.solicitado;
  • documento.firmado.externamente.

Esto transforma la aplicación de un conjunto de endpoints y documentos en una plataforma verdaderamente orientada a eventos.

Integración de servicios

Firestore

Firestore actúa como gran fuente de eventos de negocio. Los documentos cambian y esos cambios desencadenan reacciones. Es probablemente el origen más frecuente de triggers en una app Firebase.

Authentication

Authentication aporta eventos del ciclo de vida del usuario y permite automatizar altas, bajas y procesos asociados al registro o al acceso.[cite:4]

Storage

Storage introduce el mundo de los archivos: fotografías, evidencias, PDFs, documentos institucionales. Sus triggers son ideales para canalizaciones de procesamiento documental e imagen.[cite:2]

APIs externas

Aunque las APIs externas no “disparan” necesariamente eventos Firebase por sí mismas, una función activada por Firestore, Storage o Pub/Sub puede integrarse con ellas como siguiente paso del flujo. Por ejemplo, tras generarse una credencial, se puede notificar un sistema externo.

Cloud Run

Dado que Cloud Functions v2 se apoya en infraestructura basada en Cloud Run en su arquitectura general, puede convivir con servicios Cloud Run más especializados dentro de una solución distribuida. Esto es útil cuando parte del flujo necesita más control o tiempos de ejecución distintos al perfil típico de una función serverless.[cite:3]

Ejemplos paso a paso

Ejemplo 1: Crear automáticamente el perfil de un usuario

En el proyecto del libro, una necesidad central es garantizar que todo usuario autenticado tenga un documento de perfil en Firestore. Si ese proceso se deja al cliente, la consistencia puede romperse. Por eso debe automatizarse en backend.

Debido a la evolución de soporte entre generaciones, una estrategia moderna y robusta consiste en reaccionar a un evento controlado del dominio o a la creación del documento base de usuario en Firestore, si el flujo del proyecto se diseña así. Ese patrón mantiene coherencia con el principio de Cloud Functions v2 y además evita acoplar la lógica a comportamientos no siempre uniformes entre clientes.[cite:1][cite:3][cite:4]

import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { getFirestore, FieldValue } from "firebase-admin/firestore";
import { initializeApp } from "firebase-admin/app";

initializeApp();
const db = getFirestore();

export const crearPerfilUsuario = onDocumentCreated("usuariosBase/{uid}", async (event) => {
  const snapshot = event.data;
  if (!snapshot) return;

  const base = snapshot.data();
  const uid = event.params.uid;

  await db.doc(`perfiles/${uid}`).set(
    {
      uid,
      nombreCompleto: base.nombreCompleto ?? "",
      email: base.email ?? "",
      rol: base.rol ?? "estudiante",
      activo: true,
      createdAt: FieldValue.serverTimestamp(),
    },
    { merge: true }
  );
});

¿Por qué usar Trigger y no cliente?

Porque el perfil es parte del estado consistente del sistema, no un efecto opcional de interfaz. El backend debe garantizarlo.

Ejemplo 2: Generar una credencial digital al registrarse

Aquí conviene pensar por etapas. El trigger inicial no debería generar directamente un PDF complejo dentro de una única función si el proceso puede crecer. Una estrategia profesional es:

  1. Se crea el perfil.
  2. Se genera una solicitud de credencial.
  3. Otro trigger toma esa solicitud y crea los artefactos documentales.
  4. Se actualiza el estado del usuario.

Este patrón desacopla el flujo y facilita reintentos.

import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { getFirestore, FieldValue } from "firebase-admin/firestore";

const db = getFirestore();

export const solicitarCredencialAutomatica = onDocumentCreated("perfiles/{uid}", async (event) => {
  const snapshot = event.data;
  if (!snapshot) return;

  const perfil = snapshot.data();

  await db.collection("solicitudesCredencial").add({
    uid: event.params.uid,
    nombreCompleto: perfil.nombreCompleto,
    estado: "pendiente",
    origen: "trigger_perfil_creado",
    createdAt: FieldValue.serverTimestamp(),
  });
});

Ejemplo 3: Enviar correo de bienvenida

El envío de correo es un candidato clásico para automatización porque no debe depender de que el usuario permanezca conectado. El momento exacto del envío puede nacer de un trigger de creación de perfil, de alta de usuario o de un evento publicado a Pub/Sub.

import { onDocumentCreated } from "firebase-functions/v2/firestore";
import * as logger from "firebase-functions/logger";

export const encolarCorreoBienvenida = onDocumentCreated("perfiles/{uid}", async (event) => {
  const perfil = event.data?.data();
  if (!perfil?.email) return;

  logger.info("Correo de bienvenida preparado", {
    uid: event.params.uid,
    email: perfil.email,
  });

  // Aquí podría publicarse un evento o escribirse en una cola documental.
});

Ejemplo 4: Crear miniaturas de fotografías

Este caso debe ejecutarse mediante trigger y no desde el cliente por varias razones:

  • necesita uniformidad de formato;
  • puede consumir CPU adicional;
  • no debe confiar en el dispositivo del usuario;
  • debe ejecutarse incluso si la app se cierra tras la carga.

La base ya se vio en Storage triggers, pero en el proyecto del libro el flujo podría completar además metadatos en Firestore.

import { onObjectFinalized } from "firebase-functions/v2/storage";
import { getFirestore } from "firebase-admin/firestore";
import { getStorage } from "firebase-admin/storage";
import * as logger from "firebase-functions/logger";
import path from "node:path";
import sharp from "sharp";

const db = getFirestore();

export const procesarFotografiaCredencial = onObjectFinalized(async (event) => {
  const filePath = event.data.name;
  const contentType = event.data.contentType;
  const bucketName = event.data.bucket;

  if (!filePath || !contentType?.startsWith("image/")) return;

  const fileName = path.basename(filePath);
  if (fileName.startsWith("thumb_")) return;

  const uid = path.dirname(filePath).split("/").pop();
  if (!uid) return;

  const bucket = getStorage().bucket(bucketName);
  const [sourceBuffer] = await bucket.file(filePath).download();
  const thumbnailBuffer = await sharp(sourceBuffer)
    .resize({ width: 300, height: 300, fit: "cover" })
    .jpeg({ quality: 85 })
    .toBuffer();

  const thumbPath = path.join(path.dirname(filePath), `thumb_${fileName}`);
  await bucket.file(thumbPath).save(thumbnailBuffer, {
    metadata: { contentType: "image/jpeg" },
  });

  await db.doc(`perfiles/${uid}`).set(
    {
      fotoProcesada: true,
      fotoMiniaturaPath: thumbPath,
    },
    { merge: true }
  );

  logger.info("Fotografía procesada", { uid, thumbPath });
});

Ejemplo 5: Actualizar estadísticas automáticamente

Las estadísticas agregadas no deberían recalcularse desde el cliente, porque eso introduce inconsistencia y acopla la lógica analítica a la interfaz. Un trigger puede hacerlo de manera centralizada.

import { onDocumentWritten } from "firebase-functions/v2/firestore";
import { getFirestore, FieldValue } from "firebase-admin/firestore";

const db = getFirestore();

export const actualizarEstadisticasInscripciones = onDocumentWritten(
  "inscripciones/{id}",
  async (event) => {
    const before = event.data.before?.data();
    const after = event.data.after?.data();

    if (before?.estatus === after?.estatus && before?.programaId === after?.programaId) {
      return;
    }

    await db.doc("estadisticas/global").set(
      {
        updatedAt: FieldValue.serverTimestamp(),
        requiereRecalculoInscripciones: true,
      },
      { merge: true }
    );
  }
);

Aquí conviene notar una decisión importante: el trigger no recalcula todo en caliente dentro de la misma ejecución. Solo marca la necesidad o lanza la siguiente fase. Esto reduce acoplamiento y mejora resiliencia.

Ejemplo 6: Registrar auditoría

El registro de auditoría es uno de los procesos que más claramente deben vivir en backend. Si se deja al cliente, puede omitirse o manipularse.

Los triggers con auth context en Firestore son especialmente interesantes aquí, porque permiten acceder a información adicional sobre el principal que provocó el evento en ciertos tipos de trigger, como onDocumentWrittenWithAuthContext.[cite:1]

import { onDocumentWrittenWithAuthContext } from "firebase-functions/v2/firestore";
import { getFirestore, FieldValue } from "firebase-admin/firestore";

const db = getFirestore();

export const auditarCambiosPerfil = onDocumentWrittenWithAuthContext(
  "perfiles/{uid}",
  async (event) => {
    const before = event.data.before?.data() ?? null;
    const after = event.data.after?.data() ?? null;

    await db.collection("auditoria").add({
      entidad: "perfil",
      uid: event.params.uid,
      authType: event.authType,
      authId: event.authId,
      before,
      after,
      createdAt: FieldValue.serverTimestamp(),
    });
  }
);

Ejemplo 7: Sincronizar información entre colecciones

La sincronización entre colecciones es útil cuando se necesita:

  • una proyección resumida para paneles;
  • un documento de lectura optimizada;
  • un índice auxiliar;
  • una vista desacoplada del documento principal.
import { onDocumentUpdated } from "firebase-functions/v2/firestore";
import { getFirestore } from "firebase-admin/firestore";

const db = getFirestore();

export const sincronizarResumenAlumno = onDocumentUpdated("perfiles/{uid}", async (event) => {
  const before = event.data.before.data();
  const after = event.data.after.data();

  if (
    before.nombreCompleto === after.nombreCompleto &&
    before.fotoMiniaturaPath === after.fotoMiniaturaPath &&
    before.rol === after.rol
  ) {
    return;
  }

  await db.doc(`resumenAlumnos/${event.params.uid}`).set(
    {
      nombreCompleto: after.nombreCompleto,
      rol: after.rol,
      fotoMiniaturaPath: after.fotoMiniaturaPath ?? null,
      actualizadoEn: new Date().toISOString(),
    },
    { merge: true }
  );
});

Casos reales

Crear automáticamente el perfil de un usuario

Debe ejecutarse mediante trigger porque es una responsabilidad estructural del sistema. El cliente no es una fuente suficientemente confiable para garantizar consistencia entre Authentication y Firestore.

Generar una credencial digital al registrarse

Debe ejecutarse en backend porque implica reglas de negocio, potencial generación documental, acceso a datos consolidados y posibles integraciones con almacenamiento o servicios externos.

Enviar correo de bienvenida

Debe ejecutarse en backend porque involucra credenciales sensibles, reintentos, trazabilidad y la necesidad de completarse aunque el usuario ya no esté activo en la app.

Crear miniaturas de fotografías

Debe ejecutarse en backend porque requiere procesamiento de archivos, uniformidad técnica y control del flujo documental.[cite:2]

Actualizar estadísticas

Debe ejecutarse en backend porque las estadísticas agregadas deben derivarse de fuentes confiables y centralizadas.

Registrar actividad

Debe ejecutarse en backend porque la auditoría no debe depender de un cliente que podría fallar o ser manipulado.

Sincronizar información entre colecciones

Debe ejecutarse en backend porque la proyección de lectura optimizada es una responsabilidad del sistema, no del frontend.

Comparaciones técnicas

Proceso manual vs trigger automático

Aspecto Proceso manual desde cliente Trigger automático
Dependencia del frontend Alta Baja
Consistencia Variable Mayor
Ejecución sin usuario conectado No garantizada
Exposición a manipulación Mayor Menor
Automatización encadenada Limitada Natural
Escalabilidad operativa Menor Mejor alineada con eventos

Firestore Trigger vs Storage Trigger

Criterio Firestore Trigger Storage Trigger
Fuente del evento Cambios en documentos [cite:1] Cambios en objetos del bucket [cite:2]
Tipo de datos Estado de negocio Archivos y metadatos
Uso típico perfiles, estados, estadísticas imágenes, documentos, derivados
Riesgo principal loops por escribir sobre el mismo documento [cite:1] reprocesamiento del mismo archivo o derivados [cite:2]
Estrategia de salida comparar before/after [cite:1] validar contentType, nombre y metadatos [cite:2]

Trigger vs HTTP/Callable

Criterio Trigger HTTP/Callable
Activación Automática por evento Explícita por solicitud
Usuario interactúa directamente No necesariamente Sí o indirectamente
Ideal para automatización y reacción operaciones bajo demanda
Dependencia del cliente Baja Media a alta
Encadenamiento de procesos Muy natural Más explícito

Optimización

Evitar ejecuciones innecesarias

La primera regla de optimización en triggers es no trabajar si no hace falta. La documentación oficial de Firestore ya establece una base importante: si una escritura no cambia realmente los datos, no genera evento update o write.[cite:1] Sin embargo, cuando sí existe evento, sigue siendo responsabilidad del desarrollador decidir si ese cambio amerita procesamiento adicional.

Estrategias:

  • comparar solo campos relevantes;
  • ignorar cambios cosméticos;
  • salir temprano;
  • separar eventos de negocio de cambios administrativos.

Idempotencia

La documentación de Firestore recuerda que los eventos se entregan al menos una vez, y que un mismo evento puede producir múltiples invocaciones. Por eso no debe asumirse mecánica exactly-once y las funciones deben ser idempotentes.[cite:1]

Esto significa que si la función se ejecuta dos veces para el mismo evento, el resultado final no debe corromper el sistema.

Formas de lograrlo:

  • usar escrituras deterministas (set con datos finales en vez de efectos acumulativos inseguros);
  • registrar un identificador de evento procesado cuando el caso lo requiera;
  • diseñar transiciones de estado que soporten reejecución;
  • usar operaciones atómicas del SDK cuando tenga sentido.

Reintentos automáticos

Cuando una función falla, la plataforma puede reintentar ciertos eventos según el tipo de trigger y configuración. Esto es útil para resiliencia, pero también obliga a pensar en idempotencia. Sin idempotencia, el reintento puede duplicar efectos secundarios.

Manejo de errores

Un error en un trigger no debe tratarse igual que en una HTTP Function. Aquí no se le responde a un usuario directamente; se registra un fallo en un flujo de automatización. Por eso conviene:

  • loggear contexto suficiente;
  • distinguir errores transitorios de errores permanentes;
  • salir temprano en casos no procesables;
  • no ocultar fallos graves detrás de try/catch silenciosos.

Rendimiento

El rendimiento en triggers depende de:

  • cercanía regional entre función y servicio origen;
  • tamaño del documento o archivo;
  • complejidad de procesamiento;
  • número de operaciones secundarias;
  • volumen de dependencias cargadas;
  • diseño del flujo encadenado.

La documentación de Firestore y Storage insiste en la importancia de elegir bien la región para evitar latencia de red significativa entre la fuente del evento y la función.[cite:1][cite:2]

Costos

Los costos en automatización aparecen principalmente por:

  • demasiadas ejecuciones innecesarias;
  • bucles o recursividad;
  • procesamiento pesado de archivos;
  • demasiadas lecturas y escrituras derivadas;
  • funciones que realizan demasiado trabajo en una sola etapa.

Automatizar no es “gratis”. Automatizar bien significa que el valor del proceso supera el costo operativo y que el diseño evita ruido innecesario.

Buenas prácticas

  • Diseñar cada trigger alrededor de un hecho de negocio claramente identificado.
  • Preferir nombres explícitos para funciones y colecciones intermedias.
  • Modularizar la lógica: trigger delgado, servicio de dominio claro.
  • Salir temprano cuando el cambio no sea relevante.[cite:1][cite:2]
  • Evitar escribir sobre el mismo documento si no es estrictamente necesario.[cite:1]
  • Si se escribe sobre el mismo documento, comparar before y after y controlar flags.
  • Usar rutas y patrones de archivo que faciliten clasificar origen y destino.
  • Mantener idempotencia porque los eventos pueden entregarse más de una vez.[cite:1]
  • Registrar logs estructurados con contexto de documento, archivo, usuario o etapa.
  • Separar automatizaciones pesadas en múltiples etapas cuando el flujo crezca.
  • Elegir región cerca del servicio origen para reducir latencia.[cite:1][cite:2]
  • Diseñar observabilidad, no solo ejecución: saber qué pasó es parte del sistema.

Errores comunes

  • Diseñar triggers como si fueran funciones HTTP y mezclar demasiadas responsabilidades.
  • Escribir sobre el mismo documento sin validar si el cambio ya ocurrió, provocando loops.[cite:1]
  • Reprocesar miniaturas o derivados porque no se filtró por nombre o tipo de archivo.[cite:2]
  • Suponer que los eventos llegan una sola vez o en orden perfecto, cuando la documentación indica entrega al menos una vez y sin garantía de orden.[cite:1]
  • Mover demasiada lógica de negocio al cliente en lugar de automatizarla en backend.
  • Encadenar procesos sin pensar en fallos parciales.
  • No alinear región entre función y servicio origen, incrementando latencia.[cite:1][cite:2]
  • Usar onDocumentWritten para todo, en lugar de elegir triggers más expresivos cuando corresponde.
  • No diseñar idempotencia y sufrir duplicados en auditoría, estadísticas o documentos derivados.
  • No prever el impacto económico de automatizaciones muy frecuentes.

Resumen

Los triggers de Cloud Functions permiten construir aplicaciones que reaccionan automáticamente a eventos relevantes del sistema. Esta capacidad transforma Firebase de una plataforma orientada a almacenamiento y frontend en una arquitectura verdaderamente reactiva, donde Firestore, Storage y otros servicios se convierten en productores de hechos que desencadenan procesos de negocio.[cite:1][cite:2]

Firestore triggers son la base de muchas automatizaciones porque operan sobre el estado principal de la aplicación. Permiten detectar creación, actualización, eliminación o escritura general de documentos, acceder a snapshots antes y después del cambio y decidir con precisión si una transición justifica lógica adicional.[cite:1] Pero esa potencia exige disciplina: escribir sobre el mismo documento sin criterio puede introducir recursividad infinita.

Storage triggers amplían la automatización al mundo de los archivos. Gracias a ellos, una aplicación puede reaccionar automáticamente a la subida de imágenes o documentos, procesar miniaturas, convertir formatos y enriquecer metadatos sin depender del dispositivo del usuario.[cite:2] En sistemas documentales o de credenciales, este patrón es especialmente valioso.

Eventarc y Pub/Sub amplían todavía más el horizonte. Permiten desacoplar procesos, reaccionar a más fuentes de eventos y construir automatizaciones multipaso más cercanas a sistemas distribuidos profesionales.[cite:3] Esta evolución confirma que Cloud Functions v2 no debe entenderse solo como “funciones mejoradas”, sino como una plataforma de backend reactivo más completa.

En el proyecto transversal del libro, los triggers serán responsables de procesos que deben ocurrir siempre, aunque el usuario no espere a que terminen: crear perfiles, generar solicitudes de credencial, procesar fotografías, actualizar estadísticas, registrar auditoría y sincronizar proyecciones de datos. Ese es el verdadero valor de la automatización basada en eventos: garantizar consistencia operativa, reducir dependencia del cliente y preparar la aplicación para crecer como sistema distribuido real.

Conceptos clave

  • Trigger.
  • Evento.
  • Arquitectura orientada a eventos.
  • onDocumentCreated.[cite:1]
  • onDocumentUpdated.[cite:1]
  • onDocumentDeleted.[cite:1]
  • onDocumentWritten.[cite:1]
  • before y after snapshots.[cite:1]
  • Auth Context en Firestore triggers.[cite:1]
  • onObjectFinalized.[cite:2]
  • onObjectDeleted.[cite:2]
  • Idempotencia.[cite:1]
  • Recursividad.
  • Eventarc.[cite:3]
  • Pub/Sub.[cite:3]
  • Entrega al menos una vez.[cite:1]
  • Región y latencia.[cite:1][cite:2]

Preguntas de repaso

  1. ¿Qué es un trigger y en qué se diferencia de una HTTP Function o Callable Function?
  2. ¿Por qué la arquitectura orientada a eventos favorece la automatización de procesos?
  3. ¿Cuándo conviene usar onDocumentCreated en lugar de onDocumentUpdated?[cite:1]
  4. ¿Qué información proporcionan event.data.before y event.data.after?[cite:1]
  5. ¿Por qué escribir sobre el mismo documento que activó la función puede provocar bucles infinitos?[cite:1]
  6. ¿Qué ventajas ofrece onObjectFinalized para procesamiento de imágenes?[cite:2]
  7. ¿Qué papel desempeñan Eventarc y Pub/Sub en una arquitectura de automatización moderna?[cite:3]
  8. ¿Por qué la idempotencia es esencial si los eventos pueden entregarse más de una vez?[cite:1]
  9. ¿Qué procesos del proyecto del libro deben ejecutarse mediante triggers y no desde el cliente?
  10. ¿Cómo influyen región, latencia, costo y número de ejecuciones en el diseño de automatizaciones?

Ejercicios prácticos

  1. Diseña un mapa de eventos del proyecto oficial indicando qué hechos deben disparar automatizaciones.
  2. Implementa un onDocumentCreated que cree una colección de auditoría derivada cuando aparezca una nueva solicitud de credencial.[cite:1]
  3. Implementa un onDocumentUpdated que detecte cambios relevantes en estatus y salga temprano cuando no exista transición real.[cite:1]
  4. Implementa un onObjectFinalized que procese imágenes y evite reprocesar miniaturas mediante prefijos de archivo.[cite:2]
  5. Diseña un flujo de automatización para enviar correo de bienvenida sin depender del frontend.
  6. Propón una estrategia de idempotencia para evitar duplicados en generación de credenciales o auditoría.[cite:1]
  7. Reestructura una automatización compleja en varias etapas desacopladas usando una colección intermedia o Pub/Sub.
  8. Diseña una función de auditoría usando onDocumentWrittenWithAuthContext y explica qué ventajas ofrece sobre un log desde cliente.[cite:1]
  9. Evalúa qué automatización del proyecto tendría mayor impacto en costos y cómo la optimizarías.
  10. Diseña un diagrama técnico de la arquitectura de automatización completa del proyecto del libro.

Bibliografía y referencias oficiales