Construindo uma Página de Registro de Festa com Serverless e MongoDB

Este arquivo foi traduzido automaticamente por IA, erros podem ocorrer
Todos os anos, minha esposa e eu fazemos uma grande festa à fantasia no verão. Cerca de 50 dos nossos amigos aparecem à fantasia, temos jogos, um quiz, comida e muita diversão. Mas, todo ano é a mesma coisa: rastrear convites, rastrear RSVPs, enviar as mesmas informações repetidamente. Tentei resolver isso com um evento do Facebook, mas algumas pessoas se recusam a usar o Facebook, algumas só RSVP para si mesmas, deixando-me pensando "O seu parceiro também está vindo?"
Então este ano o construtor em mim acordou e construí um aplicativo web para isso.
Os convidados recebem um código pessoal, fazem login, RSVP, verificam o código de vestimenta. Eu tenho um painel de administração onde posso ver quem está vindo, quem está me ignorando, rastrear atividade e ver quais restrições alimentares preciso considerar quando for fazer as compras.
Começou como um projeto divertido de fim de semana. Mas quanto mais construí, mais percebi que isso poderia funcionar para praticamente qualquer evento. A arquitetura permanece a mesma, seja para 50 ou 500 convidados.
O código está disponível no Serverless Handbook.
Visão geral da arquitetura
Então esta é a arquitetura básica: serverless e um banco de dados, neste caso MongoDB.

O frontend é um aplicativo React 19 construído com Vite, hospedado no S3 atrás do CloudFront. Tailwind para estilização. Nada incomum aí também.
O backend é onde fica mais interessante. Funções Lambda atrás do API Gateway, com Lambda authorizers separados controlando o acesso através do padrão PDP/PEP. Escrevi sobre esse padrão em detalhes em uma postagem anterior se você quiser a imagem completa.
Para o banco de dados, escolhi o MongoDB Atlas. Mais sobre isso mais adiante, mas em resumo, tentei o DynamoDB, mas me cansei de tantas GSIs diferentes.
A autenticação é JWT auto-assinada, semelhante ao meu AI Bartender. O PDP (serviço de autenticação central) assina tokens com uma chave privada. Os Lambda Authorizers personalizados no API Gateway (PEP) apenas buscam a chave pública e podem validar o JWT. Simples e difícil de errar.
Tudo é definido em templates SAM e desta vez eu implantei na região eu-north-1 porque estou na Suécia e meus convidados são locais.
Autenticação no Atlas
A autenticação no Atlas poderia, é claro, ser feita com nome de usuário e senha tradicionais. Mas na minha postagem anterior, introduzi a Federação de Identidade Saída com MongoDB. E é claro que é isso que eu uso.
Autenticação de usuário facilitada
Eu não queria que meus convidados tivessem que se registrar e definir um nome de usuário e senha. Meus convidados estão fazendo RSVP para uma festa no jardim, não se inscrevendo para um produto SaaS. Então eu queria algo simples, no final optei por usar um token pessoal associado ao seu sobrenome.
Cada convidado recebe um código que eu gero no painel de administração. Eles digitam seu código e sobrenome. Isso é o único login.
Então este é o fluxo de login do convidado.

- Frontend (Usuário) envia o código e sobrenome para
POST /auth/login - API Gateway roteia para um Lambda Proxy de Login
- Esse Lambda chama diretamente o Lambda PDP (Lambda-to-Lambda)
- PDP procura o token no MongoDB, verifica o sobrenome
- Se corresponder, assina um JWT com a chave privada RSA
- Frontend armazena o JWT, envia como um token Bearer em cada solicitação posterior
O JWT contém o ID do convidado, nome e se ele é um administrador. Assinado com RS256, então o PDP possui a chave privada e todo o resto só precisa da chave pública. Sem segredos compartilhados.
Agora, a chamada Lambda-to-Lambda quando o Login chama o PDP, isso está longe de ser uma prática recomendada, às vezes é OK, e nesta solução pequena e simples eu optei por isso. Em uma configuração de produção, meu PDP estaria atrás de um API Gateway, sem Lambda-to-Lambda nesse caso.
Eu disse que essa solução poderia ser usada para qualquer evento de qualquer tamanho, e substituiríamos nosso pequeno fluxo de login por um registro adequado usando Cognito User Pools, Okta, Auth0 ou similares. Adicionar Stripe Checkout na frente disso e teríamos ingressos pagos também.
Por que MongoDB e não DynamoDB
Na minha primeira versão, usei o DynamoDB, funcionou bem para 50 convidados. Então comecei a adicionar funcionalidade, possibilidade de conectar convidados, rastreamento de atividade e mais. Adicionei alguns índices adicionais, mas ainda acabei com vários escaneamentos.
Como exemplo, no painel de administração, eu listo todos os convidados. Com o DynamoDB, isso resultou em um Scan. Claro, eu provavelmente poderia adicionar um índice onde a Chave de Partição seria um nome falso de festa e o ID do convidado como Chave de Ordenação. Mas não era isso que eu queria.
Depois veio a filtragem como Mostre-me todos que não fizeram RSVP. Isso foi mais um scan e filtro no seu código, ou eu poderia construir um GSI novamente, para cada padrão de consulta que agora poderia pensar.
Então comecei a pensar, mas e se um evento tiver 500, 1000 ou 10.000 convidados, continuar fazendo escaneamentos não seria tão bom.
E então vieram os relacionamentos. Minha festa tem casais e famílias. John e Jane são um par, eles compartilham um RSVP. Eu modelei isso no DynamoDB com um campo connectedTo, um link bidirecional entre registros. Bom para duas pessoas. Mas uma família de quatro? Um grupo de amigos? Conectar dois convidados significa atualizar ambos os registros. Desconectar significa encontrar a outra pessoa e atualizar ambos novamente. Ficou bagunçado rapidamente.
Agregação..... Quantos convidados estão vindo no total? escanear tudo na função Lambda, percorrê-lo, somar.
Parecia mais que eu precisava de um Banco de Dados Relacional onde eu poderia fazer consultas, ordenar, filtrar e agregação. Ao mesmo tempo, eu precisava da possibilidade de um número dinâmico de colunas (chaves) por convidado, que é um dos grandes pontos fortes do DynamoDB.
Foi aí que decidi mudar para o MongoDB Atlas e obter o melhor dos dois mundos.
O Modelo de Dados do MongoDB
Decidi ir com uma coleção: um documento por convidado. Sem joins e sem referências para perseguir.
{
"_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")
}O campo interessante é groupId. Esse é o modelo inteiro de relacionamento, assim é como casais e grupos estão conectados agora.
Grupos em vez de Pares
No DynamoDB eu tinha connectedTo: "jane-uuid" no registro de John e connectedTo: "john-uuid" no de Jane. Links bidirecionais. Funciona para duas pessoas, mas não para grupos.
A versão MongoDB é mais simples. Todos em uma família ou grupo compartilham o mesmo groupId. Sem atualizar dois registros apenas para conectar pessoas.
Para um casal seria:
{ firstName: "John", groupId: "doe-family", expectedGuests: 1 }
{ firstName: "Jane", groupId: "doe-family", expectedGuests: 1 }E para uma família de 4 é apenas o mesmo:
{ 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 }Mas o que é esse expectedGuests e o que ele traz e como funciona? O expectedGuests de cada membro inclui a si mesmo mais qualquer pessoa que estejam trazendo que não tenha seu próprio login. Por exemplo, em uma família com crianças pequenas, as crianças não teriam seu próprio login, então o expectedGuests nos pais seria > 1.
Se eu quiser adicionar alguém aos grupos agora, é uma atualização do documento dessa pessoa.
db.guests.update_one(
{"_id": new_member_id},
{"$set": {"groupId": existing_group_id}}
)Remover alguém? A mesma coisa, uma atualização.
db.guests.update_one(
{"_id": member_id},
{"$set": {"groupId": None}}
)Compare isso com a versão DynamoDB onde conectar dois convidados significava atualizar ambos os registros, verificar se nenhum já estava conectado a outra pessoa e lidar com os casos de erro para ambas as escritas.
Índices
Mudar para o MongoDB não significa que posso pular índices. Ainda preciso deles, assim como faria com um banco de dados relacional como PostgreSQL ou MySQL. Mas um índice do MongoDB (ou um índice do PostgreSQL) e um GSI do DynamoDB não são a mesma coisa. Eles compartilham um nome e é só.
Um índice no MongoDB ou PostgreSQL é uma estrutura de dados leve. Um ponteiro, basicamente diz "se você está procurando documentos onde status é pending, aqui está onde eles estão." Os dados originais permanecem onde estão. O índice apenas torna as buscas rápidas. Posso criar um em qualquer campo a qualquer momento, e isso só custa um pouco de armazenamento e alguma sobrecarga de escrita.
Um GSI no DynamoDB, isso é uma cópia completa dos dados. Quando crio um GSI, o DynamoDB duplica cada item, ou os atributos que decido projetar, em uma estrutura separada com sua própria Chave de Partição e Chave de Ordenação. Eu pagaria pelo armazenamento em ambas as cópias. Pago pela capacidade de escrita em ambas.
Essa diferença importa muito. No MongoDB, adicionar um índice em status custa quase nada e posso consultá-lo da maneira que quiser, igual, não igual, intervalos, regex, o que for. No DynamoDB, adicionar um GSI significa decidir antecipadamente qual atributo é a chave de partição. Cada novo padrão de acesso potencialmente significa um novo GSI com seu próprio custo e trade-offs de consistência eventual.
Então quando digo que meus índices do MongoDB são mais flexíveis, é isso que quero dizer.
Então eu tenho quatro índices. Cada um existe por causa de uma consulta real que o aplicativo executa.
# Login lookup. Cada login aciona isso. Deve ser único.
guests.create_index("token", unique=True)
# Group lookup. Cada RSVP busca todos os membros do grupo.
guests.create_index("groupId", sparse=True)
# Status filter. Painel de admin filtra por pendente/vindo/recusado.
guests.create_index("status")
# Composto: status + dietary. Relatório de alergias para convidados que estão vindo.
guests.create_index([("status", 1), ("dietary", 1)])O sparse: True em groupId significa que convidados solitários com groupId: null não estão no índice. Isso economiza um pouco de espaço, mas não afeta o desempenho das consultas, pois só busco por groupId quando sei que existe, não tenho necessidade de encontrar qualquer convidado sem conexão, pelo menos não agora.
Configurações
Preciso definir um prazo para RSVP, então se qualquer convidado tentar fazer RSVP ou alterar seu RSVP após essa data, ele realmente deve me ligar ou enviar uma mensagem de texto diretamente. Após uma determinada data, não quero continuar verificando alterações de RSVP, pois é muito próximo da data real da festa.
No DynamoDB eu armazenei o prazo de RSVP na mesma tabela que os convidados, usando o design de tabela única.
No MongoDB decidi dar às configurações sua própria coleção:
db.settings.find_one({"_id": "app-settings"}){
"_id": "app-settings",
"rsvpDeadline": ISODate("YYYY-MM-DDT23:59:59Z"),
"eventName": "Festa de Jimmy!",
"eventDate": ISODate("YYYY-MM-DDT16:00:00Z")
}O Fluxo de RSVP
É aqui que o modelo de grupo realmente paga e posso construir uma lógica ótima.
Então este é o fluxo de RSVP do convidado.

Quando alguém abre a página de RSVP, o aplicativo primeiro obtém seu registro e todos em seu grupo, e calcula a contagem esperada do 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)O frontend mostra a contagem esperada de convidados e seus membros do grupo. Quando eles enviam, o aplicativo compara o que eles digitou com o total esperado.
Se os números baterem, é ótimo, todos no grupo estão vindo. Uma atualização em massa marca todo o grupo como coming.
Se o convidado entrar com menos pessoas do que o esperado, um diálogo pergunta ao seu parceiro, ou a quem no grupo, que não está vindo. Esperado 2, mas você disse 1. Lisa não está vindo? E se eles disserem mais do que o esperado, o aplicativo verifica Esperado 2, mas você disse 3. Tem certeza?
A atualização em massa é uma linha.
db.guests.update_many(
{"groupId": guest["groupId"], "_id": {"$ne": guest_id}},
{"$set": {"status": "coming", "numGuests": 0, "rsvpDate": datetime.utcnow()}}
)O Painel de Administração
É aqui que passo meu tempo no aplicativo, verificando quem está vindo e quem ainda nem fez login. Quanto mais próximo da data da festa, mais tempo passo verificando esta página. Provavelmente preciso construir um sistema de notificação que faça isso por mim.
A visão geral me dá os números que me importam, convidados convidados, total de convidados vindo, alergias. Tudo de uma única agregação de banco de dados.
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
]}},
}}
])Depois há a página da lista de convidados, é onde eu adiciono pessoas, gero tokens, defino contagem esperada de convidados, conecto a um grupo e muito mais.
A parte de administração também consiste em um resumo de pessoas que fizeram RSVP e um resumo de todas as preferências alimentares e alergias. Não é tão interessante, apenas uma consulta de agregação direta ao MongoDB, assim como acima.
Rastreamento de Atividade
Decidi que preciso rastrear se meus convidados fizeram login e quando acessaram a página pela última vez. Fiz isso para que, quando a festa estiver se aproximando, eu possa ver se pessoas que não fizeram RSVP pelo menos fizeram login uma vez. Então adicionei os timestamps lastLogin e lastAccess. Escrever no banco de dados de forma síncrona em cada chamada de API parecia adicionar latência.
Portanto, adicionei uma fila SQS onde uma função Lambda separada pode operações em lote para o banco de dados. Certos eventos na página adicionam uma mensagem a uma fila. Se a publicação SQS falhar, a chamada API ainda funciona. O rastreamento é da melhor esforço, que é tudo que eu preciso para "eles pelo menos abriram a página."
Autorização com PDP e PEP
Abordei o padrão PDP/PEP e assinatura de JWT anteriormente. Aqui está como os dois authorizers se mapeiam para as terminações da API:
GET /rsvp,PUT /rsvpusam o authorizer de convidado (qualquer JWT válido passa)GET /admin/guests,POST /admin/guests, etc. usam o authorizer de admin (verifica seisAdminé verdadeiro)POST /auth/loginnão usa authorizer (público)
Dimensionando Isso para Eventos Reais
Isso funciona para minha festa de 50 pessoas no jardim. Mas, isso poderia dimensionar para uma conferência de 10.000 pessoas? Honestamente, sim, poderia, o serverless dimensionaria muito bem e o modelo de dados do MongoDB parece sólido. Claro que haveria necessidade de algumas ajustes, mas a maior parte do trabalho já está feita.
Mais convidados significa apenas mais documentos no MongoDB. Os índices já cobrem os padrões de consulta, e nesta escala, a paginação skip e limit é boa. Se eu precisar de registro público em vez de códigos de convite, troque por Cognito User Pools, os authorizers não se importam, apenas verificarão o JWT.
Para eventos pagos, colocaria o Stripe Checkout na frente do registro. Um Webhook dispara no pagamento, o backend cria o registro do convidado, envia uma confirmação, a página de RSVP se torna uma página de ingresso.
Transformando isso em uma solução SaaS para que possamos lidar com vários locatários e eventos, adicionaria um ID de locatário e um ID de evento ao modelo de dados. Alterar as consultas um pouco e adicionar funcionalidade extra, mas a maior parte da lógica está lá e é feita.
Palavras Finais
Um pequeno projeto de hobby que começou como "não quero perseguir RSVPs em chats de grupo" se transformou na base para uma plataforma de eventos serverless completa. O MongoDB lida com os dados, o Lambda mantém o backend serverless, e o padrão PDP/PEP me dá a autorização de que preciso.
O código está disponível no Serverless Handbook.
Confira minhas outras postagens em jimmydqv.com e me siga no LinkedIn para mais conteúdo sobre serverless.
Agora vá construir!