Creando una página de registro para fiestas con Serverless y MongoDB

Este archivo ha sido traducido automaticamente por IA, pueden ocurrir errores
Cada año, mi esposa y yo organizamos una gran fiesta de disfraces de verano. Alrededor de 50 de nuestros amigos asisten disfrazados, tenemos juegos, un quiz, comida y mucha diversión. Pero, cada año es lo mismo: rastrear invitaciones, rastrear confirmaciones de asistencia, enviar la misma información una y otra vez. Intenté resolverlo con un evento de Facebook, pero algunas personas se niegan a usar Facebook, algunas solo confirman asistencia por sí mismas, dejándome preguntándome "¿También viene tu pareja?".
Así que este año, el constructor en mí despertó y construí una aplicación web para ello.
Los invitados reciben un código personal, inician sesión, confirman su asistencia, revisan el código de vestimenta. Yo obtengo un panel de administración donde puedo ver quién viene, quién me está ignorando, rastrear la actividad y ver las restricciones dietéticas que debo tener en cuenta cuando haga la compra.
Comenzó como un proyecto divertido de fin de semana. Pero cuanto más construía, más me daba cuenta de que esto podría funcionar para prácticamente cualquier evento. La arquitectura sigue siendo la misma, ya sean 50 invitados o 500.
El código está disponible en Serverless Handbook.
Visión general de la arquitectura
Esta es la arquitectura básica: serverless y una base de datos, en este caso MongoDB.

El frontend es una aplicación React 19 construida con Vite, alojada en S3 detrás de CloudFront. Tailwind para el estilo. Nada inusual ahí tampoco.
El backend es donde se pone más interesante. Funciones Lambda detrás de API Gateway, con autorizadores Lambda separados que controlan el acceso a través del patrón PDP/PEP. Escribí sobre ese patrón en detalle en un artículo anterior si quieres la imagen completa.
Para la base de datos elegí MongoDB Atlas. Más sobre eso más adelante, pero en resumen, intenté usar DynamoDB pero me cansé de tantas GSIs diferentes.
La autenticación es JWT autofirmados, similar a mi AI Bartender. El PDP (servicio de autenticación central) firma los tokens con una clave privada. Los autorizadores Lambda personalizados en API Gateway (PEP) simplemente obtienen la clave pública y pueden validar el JWT. Simple, y difícil de estropear.
Todo está definido en plantillas SAM y esta vez lo desplegué en eu-north-1 porque estoy en Suecia y mis invitados son locales.
Autenticación con Atlas
La autenticación hacia Atlas podría hacerse con un nombre de usuario y contraseña tradicionales. Pero en mi artículo anterior introduje la federación de identidad saliente con MongoDB. Y, por supuesto, es lo que uso.
Autenticación de usuarios facilitada
No quería que mis invitados tuvieran que registrarse y configurar un nombre de usuario y contraseña. Mis invitados están confirmando asistencia para una fiesta en el jardín, no registrándose para un producto SaaS. Así que quería algo simple, al final opté por usar un token personal emparejado con su apellido.
Cada invitado recibe un código que genero en el panel de administración. Escriben su código y apellido. Eso es todo el inicio de sesión.
Así es el flujo de inicio de sesión para los invitados.

- Frontend (Usuario) envía el código y el apellido a
POST /auth/login - API Gateway redirige a una Lambda de proxy de inicio de sesión
- Esa Lambda llama directamente a la Lambda PDP (Lambda a Lambda)
- PDP busca el token en MongoDB, verifica el apellido
- Si coincide, firma un JWT con la clave privada RSA
- El frontend almacena el JWT, lo envía como un token Bearer en cada solicitud posterior
El JWT contiene el ID del invitado, nombre y si es un administrador. Firmado con RS256, por lo que el PDP tiene la clave privada y todo lo demás solo necesita la clave pública. Sin secretos compartidos.
Ahora, la llamada Lambda-a-Lambda cuando Login llama a PDP, está lejos de ser una mejor práctica, a veces está bien, y en esta pequeña y simple solución opté por eso. En una configuración de producción, mi PDP estaría detrás de un API Gateway, no habría Lambda-a-Lambda en ese caso.
Dije que esta solución podría usarse para cualquier evento de cualquier tamaño, y reemplazaríamos nuestro pequeño flujo de inicio de sesión con un registro adecuado usando Cognito User Pools, Okta, Auth0 o similar. Agregar Stripe Checkout frente a eso y tendríamos boletos pagados también.
¿Por qué MongoDB y no DynamoDB?
En mi primera versión usé DynamoDB, funcionó bien para 50 invitados. Luego comencé a agregar funcionalidad, posibilidad de conectar invitados, rastrear actividad y más. Agregué algunos índices más, pero aún terminé con varios escaneos.
Como ejemplo, en el panel de administración muestro a todos los invitados. Con DynamoDB eso resultó en un Escaneo. Claro, probablemente podría agregar un índice donde la clave de partición sería un nombre falso de fiesta y el ID del invitado como clave de orden. Pero eso no era lo que quería.
Luego vino el filtrado como Muéstrame a todos los que no han confirmado su asistencia. Esto era un escaneo más y un filtro en tu código, o podría construir un GSI nuevamente, para cada patrón de consulta que ahora pudiera pensar.
Así que comencé a pensar, pero ¿qué pasaría si un evento tiene 500, 1000 o 10,000 invitados, seguir haciendo escaneos no sería tan genial.
Y luego vinieron las relaciones. Mi fiesta tiene parejas y familias. John y Jane son una pareja, comparten una confirmación de asistencia. Modelé esto en DynamoDB con un campo connectedTo, un enlace bidireccional entre registros. Bien para dos personas. ¿Pero una familia de cuatro? ¿Un grupo de amigos? Conectar dos invitados significa actualizar ambos registros. Desconectar significa encontrar a la otra persona y actualizar ambos nuevamente. Se volvió un lío rápidamente.
Agregación..... ¿Cuántos invitados vienen en total? escanea todo en la función Lambda, lo recorre, lo suma.
Más sentí que necesitaba una base de datos relacional donde pudiera hacer consultas, ordenar, filtrar y agregación. Al mismo tiempo, necesitaba la posibilidad de un número dinámico de columnas (claves) por invitado, que es uno de los grandes poderes de DynamoDB.
Aquí es donde decidí moverme a MongoDB Atlas y obtener lo mejor de ambos mundos.
El modelo de datos de MongoDB
Decidí optar por una colección: un documento por invitado. Sin uniones y sin referencias para rastrear.
{
"_id": ObjectId("..."),
"firstName": "John",
"lastName": "Doe",
"token": "vTFSxGE3",
"status": "pending",
"numGuests": 0,
"dietary": "",
"expectedGuests": 1,
"isAdmin": false,
"groupId": "doe-family",
"rsvpDate": null,
"lastLogin": null,
"lastAccess": null,
"createdAt": ISODate("2026-04-01T18:00:00Z")
}El campo interesante es groupId. Ese es todo el modelo de relaciones, así es como ahora están conectadas las parejas y los grupos.
Grupos en lugar de pares
En DynamoDB tenía connectedTo: "jane-uuid" en el registro de John y connectedTo: "john-uuid" en el de Jane. Enlaces bidireccionales. Funciona para dos personas pero no para grupos.
La versión de MongoDB es más simple. Todos en una familia o grupo comparten el mismo groupId. No es necesario actualizar dos registros solo para conectar personas.
Para una pareja sería:
{ firstName: "John", groupId: "doe-family", expectedGuests: 1 }
{ firstName: "Jane", groupId: "doe-family", expectedGuests: 1 }Y para una familia de 4 es lo mismo:
{ firstName: "Bob", groupId: "smith-family", expectedGuests: 1 }
{ firstName: "Alice", groupId: "smith-family", expectedGuests: 1 }
{ firstName: "Tom", groupId: "smith-family", expectedGuests: 1 }
{ firstName: "Emma", groupId: "smith-family", expectedGuests: 1 }Pero, ¿qué es ese expectedGuests y qué aporta y cómo funciona? El expectedGuests de cada miembro incluye a sí mismos más cualquier persona que traigan que no tenga su propio inicio de sesión. Por ejemplo, en una familia con niños pequeños, los niños no tendrían su propio inicio de sesión, en su lugar, el expectedGuests de los padres sería > 1.
Si ahora quiero agregar a alguien a los grupos, es una actualización del documento de esa persona.
db.guests.update_one(
{"_id": new_member_id},
{"$set": {"groupId": existing_group_id}}
)¿Eliminar a alguien? Lo mismo, una actualización.
db.guests.update_one(
{"_id": member_id},
{"$set": {"groupId": None}}
)Compara eso con la versión de DynamoDB donde conectar dos invitados significaba actualizar ambos registros, verificar que ninguno ya estuviera conectado a otra persona y manejar los casos de error para ambas escrituras.
Índices
Moverme a MongoDB no significa que pueda omitir los índices. Todavía los necesito, igual que lo haría con una base de datos relacional como PostgreSQL o MySQL. Pero un índice de MongoDB (o un índice de PostgreSQL) y un GSI de DynamoDB no son lo mismo. Comparten un nombre y eso es todo.
Un índice en MongoDB o PostgreSQL es una estructura de datos ligera. Un puntero, básicamente dice "si estás buscando documentos donde status es pending, aquí están". Los datos originales se quedan donde están. El índice solo hace que las búsquedas sean rápidas. Puedo crear uno en cualquier campo en cualquier momento, y solo me cuesta un poco de almacenamiento y algo de sobrecarga de escritura.
Un GSI en DynamoDB, esa es una copia completa de los datos. Cuando creo un GSI, DynamoDB duplica cada elemento, o los atributos que decido proyectar, en una estructura separada con su propia clave de partición y clave de orden. Pagaría por el almacenamiento en ambas copias. Pago por la capacidad de escritura en ambas.
Esa diferencia importa mucho. En MongoDB, agregar un índice en status cuesta casi nada y puedo consultarlo de cualquier manera que quiera, iguales, no iguales, rangos, regex, lo que sea. En DynamoDB, agregar un GSI significa decidir de antemano qué atributo es la clave de partición. Cada nuevo patrón de acceso potencialmente significa un nuevo GSI con su propio costo y compensaciones de consistencia eventual.
Así que cuando digo que mis índices de MongoDB son más flexibles, eso es lo que quiero decir.
Así que tengo cuatro índices. Cada uno existe debido a una consulta real que hace la aplicación.
# Búsqueda de inicio de sesión. Cada inicio de sesión golpea esto. Debe ser único.
guests.create_index("token", unique=True)
# Búsqueda de grupo. Cada confirmación de asistencia obtiene a todos los miembros del grupo.
guests.create_index("groupId", sparse=True)
# Filtro de estado. El panel de administración filtra por pendiente/viene/rechazado.
guests.create_index("status")
# Compuesto: estado + dieta. Informe de alergias para los invitados que vienen.
guests.create_index([("status", 1), ("dietary", 1)])El sparse: True en groupId significa que los invitados solitarios con groupId: null no están en el índice. Ahorra un poco de espacio, pero no afecta el rendimiento de las consultas ya que solo busco por groupId cuando sé que existe, no tengo necesidad de encontrar a ningún invitado sin conexión, al menos por ahora.
Configuración
Necesito establecer una fecha límite para las confirmaciones de asistencia, de modo que si algún invitado intenta confirmar su asistencia o cambiarla después de esa fecha, en realidad deba llamarme o enviarme un mensaje de texto directamente. Después de cierta fecha no quiero seguir revisando cambios en las confirmaciones de asistencia, ya que está demasiado cerca de la fecha real de la fiesta.
En DynamoDB almacené la fecha límite de confirmaciones de asistencia en la misma tabla que los invitados, usando el diseño de tabla única.
En MongoDB decidí dar a la configuración su propia colección:
db.settings.find_one({"_id": "app-settings"}){
"_id": "app-settings",
"rsvpDeadline": ISODate("YYYY-MM-DDT23:59:59Z"),
"eventName": "¡La fiesta de Jimmy!",
"eventDate": ISODate("YYYY-MM-DDT16:00:00Z")
}El flujo de confirmación de asistencia
Aquí es donde el modelo de grupo realmente da sus frutos y puedo construir una gran lógica.
Este es el flujo de confirmación de asistencia para los invitados.

Cuando alguien abre la página de confirmación de asistencia, la aplicación primero obtiene su registro y a todos en su grupo, y calcula el conteo esperado del grupo.
guest = db.guests.find_one({"_id": guest_id})
group_members = []
family_sum = guest["expectedGuests"]
if guest.get("groupId"):
group_members = list(db.guests.find({
"groupId": guest["groupId"],
"_id": {"$ne": guest["_id"]}
}))
family_sum += sum(m["expectedGuests"] for m in group_members)El frontend muestra el conteo de invitados esperado y a los miembros de su grupo. Cuando envían, la aplicación compara lo que ingresaron contra el total esperado.
Si los números coinciden, es genial, todos en el grupo vienen. Una actualización masiva marca a todo el grupo como viene.
Si el invitado ingresa menos personas de lo esperado, un diálogo pregunta a su pareja, o a quién en el grupo, no viene. Se esperaban 2 pero dijiste 1. ¿Lisa no viene? Y si dicen más de lo esperado, la aplicación verifica Se esperaban 2 pero dijiste 3. ¿Estás seguro?
La actualización masiva es una línea.
db.guests.update_many(
{"groupId": guest["groupId"], "_id": {"$ne": guest_id}},
{"$set": {"status": "coming", "numGuests": 0, "rsvpDate": datetime.utcnow()}}
)El panel de administración
Aquí es donde paso mi tiempo en la aplicación, revisando quién viene y quién ni siquiera ha iniciado sesión. Cuanto más se acerca la fecha de la fiesta, más tiempo paso revisando esta página. Probablemente necesite construir un sistema de notificaciones que haga esto por mí.
La visión general me da los números que me importan, invitados, total de invitados que vienen, alergias. Todo desde una sola agregación de base de datos.
stats = db.guests.aggregate([
{"$group": {
"_id": None,
"totalInvited": {"$sum": 1},
"totalComing": {"$sum": {"$cond": [{"$eq": ["$status", "coming"]}, 1, 0]}},
"totalGuests": {"$sum": {"$cond": [{"$eq": ["$status", "coming"]}, "$numGuests", 0]}},
"totalExpected": {"$sum": "$expectedGuests"},
"withAllergies": {"$sum": {"$cond": [
{"$and": [{"$eq": ["$status", "coming"]}, {"$ne": ["$dietary", ""]}]},
1, 0
]}},
}}
])Luego está la página de lista de invitados, aquí es donde agrego personas, genero tokens, establezco el conteo de invitados esperado, conecto a un grupo y más.
La parte de administración también consiste en un resumen de personas que han confirmado su asistencia y un resumen de todas las preferencias alimentarias y alergias. No es tan interesante, solo una consulta de agregación directa hacia MongoDB.
Rastreamiento de actividad
Decidí que necesito rastrear si mis invitados han iniciado sesión y cuándo accedieron la página por última vez. Lo hice para que, cuando la fiesta se acerque, pueda ver si las personas que no han confirmado su asistencia al menos han iniciado sesión una vez. Así que agregué marcas de tiempo lastLogin y lastAccess. Escribir en la base de datos de manera síncrona en cada llamada de API se sentía como una latencia añadida.
Por lo tanto, agregué una cola SQS donde una función Lambda separada puede realizar operaciones en lotes hacia la base de datos. Ciertos eventos en la página agregan un mensaje a una cola. Si la publicación SQS falla, la llamada API aún funciona. El rastreamiento es lo mejor posible, que es todo lo que necesito para "al menos abrieron la página".
Autorización con PDP y PEP
Cubrí el patrón PDP/PEP y la firma de JWT anteriormente. Así es como los dos autorizadores se mapean a los puntos finales de la API:
GET /rsvp,PUT /rsvpusan el autorizador de invitados (cualquier JWT válido pasa)GET /admin/guests,POST /admin/guests, etc. usan el autorizador de administrador (verifica queisAdminsea verdadero)POST /auth/loginno usa autorizador (público)
Escalando esto para eventos reales
Esto funciona para mi fiesta en el jardín de 50 personas. Pero, ¿podría escalar a una conferencia de 10,000 personas? Honestamente, sí podría, serverless escalaría muy bien y el modelo de datos de MongoDB se siente sólido. Por supuesto, habría necesidad de algunos ajustes, pero la mayor parte del trabajo ya está hecho.
Más invitados solo significa más documentos en MongoDB. Los índices ya cubren los patrones de consulta, y a esta escala, la paginación skip y limit está bien. Si necesito registro público en lugar de códigos de invitación, intercambio por Cognito User Pools, los autorizadores no se preocupan realmente, solo verificará el JWT.
Para eventos pagados, pondría Stripe Checkout frente al registro. Un Webhook se dispara en el pago, el backend crea el registro del invitado, envía una confirmación, la página de confirmación de asistencia se convierte en una página de boletos.
Convirtiendo esto en una solución SaaS para que podamos manejar múltiples inquilinos y eventos, agregaría un ID de inquilino y un ID de evento al modelo de datos. Cambiaría un poco las consultas y agregaría alguna funcionalidad adicional, pero la mayor parte de la lógica está allí y está hecha.
Palabras finales
Un pequeño proyecto de pasatiempo que comenzó como "no quiero perseguir confirmaciones de asistencia en los chats de grupo" se convirtió en la base de una plataforma de eventos serverless completa. MongoDB maneja los datos, Lambda mantiene el backend serverless, y el patrón PDP/PEP me da la autorización que necesito.
El código está disponible en Serverless Handbook.
Visita mis otros artículos en jimmydqv.com y sígueme en LinkedIn para más contenido sobre serverless.
¡Ahora ve a construir!