Aufbau einer Party-Registrierungsseite mit Serverless und MongoDB

Diese Datei wurde automatisch von KI ubersetzt, es konnen Fehler auftreten
Jedes Jahr veranstalten meine Frau und ich eine große Kostüm-Sommerparty. Etwa 50 unserer Freunde kommen in Kostümen, es gibt Spiele, ein Quiz, Essen und eine Menge Spaß. Aber jedes Jahr ist es dasselbe: Einladungen verfolgen, RSVPs verfolgen, immer wieder dieselben Informationen versenden. Ich habe versucht, es mit einem Facebook-Event zu lösen, aber einige Leute weigern sich, Facebook zu nutzen, einige RSVP-en nur für sich selbst, sodass ich mich frage: „Kommt dein Partner auch?“
Also wachte dieses Jahr der Baumeister in mir auf und ich habe eine Web-App dafür gebaut.
Gäste erhalten einen persönlichen Code, melden sich an, RSVP-en, überprüfen die Kleiderordnung. Ich bekomme ein Admin-Dashboard, in dem ich sehen kann, wer kommt, wer mich ignoriert, die Aktivität verfolgen und welche diätetischen Einschränkungen ich bei meinem Einkaufsbummel berücksichtigen muss.
Es begann als ein lustiges Wochenendprojekt. Aber je mehr ich baute, desto mehr wurde mir klar, dass dies für praktisch jedes Event funktionieren könnte. Die Architektur bleibt dieselbe, ob es 50 oder 500 Gäste sind.
Der Code ist verfügbar auf Serverless Handbook.
Architekturüberblick
Das ist also die grundlegende Architektur: serverlos und eine Datenbank, in diesem Fall MongoDB.

Das Frontend ist eine React 19-App, die mit Vite gebaut und von S3 hinter CloudFront gehostet wird. Tailwind für das Styling. Auch hier nichts Ungewöhnliches.
Der Backend ist der interessantere Teil. Lambda-Funktionen hinter API Gateway, mit separaten Lambda-Autorisierungen, die den Zugriff über das PDP/PEP-Muster steuern. Ich habe über dieses Muster ausführlich in einem früheren Beitrag geschrieben, wenn Sie das vollständige Bild sehen möchten.
Für die Datenbank habe ich mich für MongoDB Atlas entschieden. Mehr dazu später, aber kurz gesagt: Ich habe DynamoDB ausprobiert, aber mich an so vielen verschiedenen GSIs gestört.
Die Authentifizierung erfolgt über selbstsignierte JWTs, ähnlich wie bei meinem AI Bartender. Der PDP (zentraler Authentifizierungsdienst) signiert Tokens mit einem privaten Schlüssel. Die benutzerdefinierten Lambda-Autorisierungen in API Gateway (PEP) holen einfach den öffentlichen Schlüssel ab und können das JWT validieren. Einfach und schwer zu vermasseln.
Alles ist in SAM-Templates definiert und dieses Mal habe ich in eu-north-1 bereitgestellt, weil ich in Schweden bin und meine Gäste lokal sind.
Authentifizierung zu Atlas
Die Authentifizierung gegenüber Atlas könnte natürlich mit traditionellen Benutzernamen und Passworten erfolgen. Aber in meinem vorherigen Beitrag habe ich Outbound Identity Federation mit MongoDB eingeführt. Und natürlich ist dies das, was ich verwende.
Benutzerauthentifizierung vereinfacht
Ich wollte nicht, dass meine Gäste sich registrieren und einen Benutzernamen und ein Passwort festlegen müssen. Meine Gäste RSVP-en für eine Gartenparty, nicht für ein SaaS-Produkt. Also wollte ich etwas Einfaches, am Ende habe ich mich dafür entschieden, ein persönliches Token mit ihrem Nachnamen zu verwenden.
Jeder Gast erhält einen Code, den ich im Admin-Dashboard generiere. Sie geben ihren Code und ihren Nachnamen ein. Das ist das ganze Login.
Das ist also der Gast-Login-Fluss.

- Frontend (Benutzer) sendet den Code und den Nachnamen an
POST /auth/login - API Gateway leitet an eine Login-Proxy-Lambda weiter
- Diese Lambda ruft die PDP-Lambda direkt auf (Lambda-zu-Lambda)
- PDP sucht das Token in MongoDB und überprüft den Nachnamen
- Wenn es übereinstimmt, signiert es ein JWT mit dem RSA-Privatschlüssel
- Das Frontend speichert das JWT und sendet es als Bearer-Token bei jeder anschließenden Anfrage
Das JWT enthält die Gast-ID, den Namen und ob sie Admin sind. Mit RS256 signiert, sodass der PDP den Privatschlüssel hält und alles andere nur den öffentlichen Schlüssel benötigt. Keine geteilten Geheimnisse.
Nun, der Lambda-zu-Lambda-Aufruf, wenn Login die PDP aufruft, ist weit entfernt von Best Practice, manchmal ist es OK, und in dieser kleinen, einfachen Lösung habe ich mich dafür entschieden. In einer Produktionsumgebung würde mein PDP hinter einem API Gateway stehen, kein Lambda-zu-Lambda in diesem Fall.
Ich sagte, diese Lösung könnte für jedes Event jeder Größe verwendet werden, und wir würden unseren kleinen Login-Fluss durch eine richtige Anmeldung mit Cognito User Pools, Okta, Auth0 oder ähnlichem ersetzen. Fügen Sie Stripe Checkout davor und wir bekommen bezahlte Ticketing ebenfalls.
Warum MongoDB und nicht DynamoDB
In meiner ersten Version habe ich DynamoDB verwendet, es funktionierte gut für 50 Gäste. Dann begann ich, Funktionalitäten hinzuzufügen, die Möglichkeit, Gäste zu verbinden, Aktivitätsverfolgung und mehr. Ich habe einige zusätzliche Indizes hinzugefügt, aber trotzdem bin ich auf mehrere Scans gestoßen.
Als Beispiel liste ich im Admin-Dashboard alle Gäste auf. Mit DynamoDB ergab das einen Scan. Sicher, ich könnte wahrscheinlich einen Index hinzufügen, bei dem der Partition Key ein gefälschter Partynamen und die Gast-ID als Sort Key wäre. Aber das war nicht das, was ich wollte.
Dann kam die Filterung wie Zeig mir alle, die noch nicht RSVP-ed haben. Das war ein weiterer Scan und Filter in Ihrem Code, oder ich könnte wieder einen GSI bauen, für jedes Abfragemuster, das ich mir jetzt vorstellen konnte.
Also begann ich nachzudenken, aber was ist, wenn ein Event 500, 1000 oder 10.000 Gäste hat, würden Scans weiterhin nicht so gut sein.
Und dann kamen die Beziehungen. Meine Party hat Paare und Familien. John und Jane sind ein Paar, sie teilen ein RSVP. Ich habe dies in DynamoDB mit einem connectedTo-Feld modelliert, einer bidirektionalen Verknüpfung zwischen Datensätzen. Gut für zwei Personen. Aber eine Familie von vier? Eine Gruppe von Freunden? Zwei Gäste zu verbinden bedeutet, beide Datensätze zu aktualisieren. Das Trennen bedeutet, die andere Person zu finden und beide erneut zu aktualisieren. Es wurde schnell unübersichtlich.
Aggregation..... `Wie viele Gäste kommen insgesamt?" Scanne alles in die Lambda-Funktion, durchlaufe es, summiere es.
Es fühlte sich mehr an, als bräuchte ich eine relationale Datenbank, in der ich Abfragen, Sortieren, Filtern und Aggregation durchführen könnte. Gleichzeitig brauchte ich die Möglichkeit für eine dynamische Anzahl von Spalten (Schlüsseln) pro Gast, was eine der großen Stärken von DynamoDB ist.
Hier habe ich beschlossen, zu MongoDB Atlas zu wechseln und das Beste aus beiden Welten zu nehmen.
Das MongoDB-Datenmodell
Ich habe mich für eine Sammlung entschieden: ein Dokument pro Gast. Keine Joins und keine Verweise, denen man folgen muss.
{
"_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")
}Das interessante Feld ist groupId. Das ist das ganze Beziehungsmodell, so sind Paare und Gruppen jetzt verbunden.
Gruppen anstelle von Paaren
In DynamoDB hatte ich connectedTo: "jane-uuid" in Johns Datensatz und connectedTo: "john-uuid" in Janes. Bidirektionale Links. Funktioniert für zwei Personen, aber nicht für Gruppen.
Die MongoDB-Version ist einfacher. Jeder in einer Familie oder Gruppe teilt dasselbe groupId. Keine Aktualisierung von zwei Datensätzen, nur um Leute zu verbinden.
Für ein Paar wäre es:
{ firstName: "John", groupId: "doe-family", expectedGuests: 1 }
{ firstName: "Jane", groupId: "doe-family", expectedGuests: 1 }Und für eine Familie von 4 ist es einfach dasselbe:
{ 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 }Aber was ist das expectedGuests und was bringt das und wie funktioniert es? Das expectedGuests jedes Mitglieds umfasst sich selbst plus alle, die sie mitbringen, die kein eigenes Login haben. Zum Beispiel würden bei einer Familie mit kleinen Kindern die Kinder kein eigenes Login bekommen, stattdessen wäre das expectedGuests der Eltern > 1.
Wenn ich jetzt jemanden zu den Gruppen hinzufügen möchte, ist es ein Update des Dokuments dieser Person.
db.guests.update_one(
{"_id": new_member_id},
{"$set": {"groupId": existing_group_id}}
)Jemanden entfernen? Dasselbe, ein Update.
db.guests.update_one(
{"_id": member_id},
{"$set": {"groupId": None}}
)Vergleichen Sie das mit der DynamoDB-Version, bei der das Verbinden von zwei Gästen bedeutete, beide Datensätze zu aktualisieren, zu überprüfen, dass keiner der beiden bereits mit jemand anderem verbunden ist, und die Fehlerfälle für beide Schreiboperationen zu behandeln.
Indizes
Der Wechsel zu MongoDB bedeutet nicht, dass ich Indizes überspringen kann. Ich brauche sie immer noch, genau wie bei einer relationalen Datenbank wie PostgreSQL oder MySQL. Aber ein MongoDB-Index (oder ein PostgreSQL-Index) und ein DynamoDB-GSI sind nicht dasselbe. Sie teilen einen Namen und das war's.
Ein Index in MongoDB oder PostgreSQL ist eine leichte Datenstruktur. Ein Zeiger, er sagt im Grunde: „Wenn Sie nach Dokumenten suchen, bei denen status pending ist, hier sind sie.“ Die Originaldaten bleiben, wo sie sind. Der Index macht Suchen nur schnell. Ich kann jederzeit einen Index auf jedem Feld erstellen, und es kostet mich nur ein bisschen Speicher und etwas Schreibüberhead.
Ein GSI in DynamoDB, das ist eine vollständige Kopie der Daten. Wenn ich einen GSI erstelle, dupliziert DynamoDB jedes Element oder die Attribute, die ich projizieren möchte, in eine separate Struktur mit seinem eigenen Partition Key und Sort Key. Ich würde für den Speicher auf beiden Kopien bezahlen. Ich zahle für die Schreibkapazität auf beiden.
Dieser Unterschied ist sehr wichtig. In MongoDB kostet das Hinzufügen eines Index auf status fast nichts und ich kann ihn auf jede Weise abfragen, Gleichheit, Ungleichheit, Bereiche, Regex, was auch immer. In DynamoDB bedeutet das Hinzufügen eines GSI, im Voraus zu entscheiden, welches Attribut der Partition Key ist. Jedes neue Zugriffsmuster bedeutet möglicherweise einen neuen GSI mit seinen eigenen Kosten und Eventual-Konsistenz-Kompromissen.
Wenn ich also sage, dass meine MongoDB-Indizes flexibler sind, ist das das, was ich meine.
Also habe ich vier Indizes. Jeder existiert wegen einer echten Abfrage, die die App ausführt.
# Login-Suche. Jeder Login trifft dies. Muss eindeutig sein.
guests.create_index("token", unique=True)
# Gruppen-Suche. Jedes RSVP holt alle Gruppenmitglieder ab.
guests.create_index("groupId", sparse=True)
# Statusfilter. Admin-Dashboard filtert nach pending/coming/declined.
guests.create_index("status")
# Verbund: Status + Diät. Allergiebericht für kommende Gäste.
guests.create_index([("status", 1), ("dietary", 1)])Das sparse: True bei groupId bedeutet, dass Einzelgäste mit groupId: null nicht im Index sind. Es spart ein bisschen Platz, beeinflusst aber die Abfrageleistung nicht, da ich nur nach groupId suche, wenn ich weiß, dass es existiert, ich muss jetzt nicht nach Gästen ohne Verbindung suchen.
Einstellungen
Ich muss eine RSVP-Frist festlegen, sodass Gäste, die nach diesem Datum RSVP-en oder ihre RSVP ändern möchten, mich tatsächlich anrufen oder eine SMS senden müssen. Nach einem bestimmten Datum möchte ich keine RSVP-Änderungen mehr überprüfen, da es zu nah am tatsächlichen Partydatums ist.
In DynamoDB habe ich die RSVP-Frist in derselben Tabelle wie die Gäste gespeichert, mit dem Single-Table-Design.
In MongoDB habe ich beschlossen, Einstellungen ihrer eigenen Sammlung zu geben:
db.settings.find_one({"_id": "app-settings"}){
"_id": "app-settings",
"rsvpDeadline": ISODate("YYYY-MM-DDT23:59:59Z"),
"eventName": "Jimmy's Party!",
"eventDate": ISODate("YYYY-MM-DDT16:00:00Z")
}Der RSVP-Fluss
Hier zahlt sich das Gruppenmodell wirklich aus und ich kann einige großartige Logik bauen.
Das ist also der Gast-RSVP-Fluss.

Wenn jemand die RSVP-Seite öffnet, holt die App zuerst ihren Datensatz und alle in ihrer Gruppe und berechnet die erwartete Gruppenanzahl.
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)Das Frontend zeigt die erwartete Gastanzahl und ihre Gruppenmitglieder an. Wenn sie absenden, vergleicht die App, was sie eingegeben haben, mit der erwarteten Gesamtzahl.
Wenn die Zahlen übereinstimmen, ist das großartig, alle in der Gruppe kommen. Ein Bulk-Update markiert die ganze Gruppe als coming.
Wenn der Gast weniger Personen als erwartet angibt, fragt ein Dialog seinen Partner oder wer in der Gruppe nicht kommt, an. Erwartet 2, aber du hast 1 gesagt. Kommt Lisa nicht?" Und wenn sie mehr als erwartet angeben, überprüft die App noch einmalErwartet 2, aber du hast 3 gesagt. Sicher damit?`
Das Bulk-Update ist eine Zeile.
db.guests.update_many(
{"groupId": guest["groupId"], "_id": {"$ne": guest_id}},
{"$set": {"status": "coming", "numGuests": 0, "rsvpDate": datetime.utcnow()}}
)Das Admin-Dashboard
Hier verbringe ich meine Zeit in der App, überprüfe, wer kommt und wer noch nicht einmal eingeloggt hat. Je näher das Partydatums rückt, desto mehr Zeit verbringe ich damit, diese Seite zu überprüfen. Ich muss wahrscheinlich ein Benachrichtigungssystem bauen, das das für mich erledigt.
Der Überblick gibt mir die Zahlen, die mich interessieren, eingeladene Gäste, insgesamt kommende Gäste, Allergien. Alles aus einer einzigen Datenbankaggregation.
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
]}},
}}
])Dann gibt es die Gästelisten-Seite, hier füge ich Leute hinzu, generiere Tokens, lege die erwartete Gastanzahl fest, verbinde mit einer Gruppe und mehr.
Der Admin-Teil besteht auch aus einer Zusammenfassung der Leute, die RSVP-ed haben, und einer Zusammenfassung aller Lebensmittelvorlieben und Allergien. Nicht so interessant, nur wie oben eine einfache Aggregationsabfrage an MongoDB.
Aktivitätsverfolgung
Ich habe beschlossen, dass ich verfolgen muss, ob meine Gäste sich überhaupt eingeloggt haben und wann sie die Seite das letzte Mal aufgerufen haben. Ich habe das getan, damit ich sehen kann, ob Leute, die noch nicht RSVP-ed haben, zumindest einmal eingeloggt haben, wenn die Party näher rückt. Also habe ich lastLogin und lastAccess-Zeitstempel hinzugefügt. Das Schreiben in die Datenbank auf synchrone Weise bei jedem einzelnen API-Aufruf fühlte sich wie zusätzliche Latenz an.
Ich habe daher eine SQS-Warteschlange hinzugefügt, wo eine separate Lambda-Funktion Batch-Operationen gegenüber der Datenbank ausführen kann. Bestimmte Ereignisse auf der Seite fügen eine Nachricht zu einer Warteschlange hinzu. Wenn die SQS-Publikation fehlschlägt, funktioniert der API-Aufruf trotzdem. Die Verfolgung ist Bestes, was ich für „haben sie zumindest die Seite geöffnet“ brauche.
Autorisierung mit PDP und PEP
Ich habe das PDP/PEP-Muster und die JWT-Signierung bereits behandelt. So ordnen sich die beiden Autorisierungen den API-Endpunkten zu:
GET /rsvp,PUT /rsvpverwenden den Gast-Autorisierungsdienst (jedes gültige JWT passt)GET /admin/guests,POST /admin/guests, usw. verwenden den Admin-Autorisierungsdienst (überprüft, obisAdminwahr ist)POST /auth/loginverwendet keine Autorisierung (öffentlich)
Skalierung für echte Events
Dies funktioniert für meine Gartenparty mit 50 Personen. Aber könnte es auf eine Konferenz mit 10.000 Personen skaliert werden? Ehrlich gesagt, ja, es könnte, Serverless würde großartig skalieren und das MongoDB-Datenmodell fühlt sich solide an. Natürlich gäbe es einige Anpassungen, aber der größte Teil der Arbeit ist bereits erledigt.
Mehr Gäste bedeuten nur mehr Dokumente in MongoDB. Die Indizes decken bereits die Abfragemuster ab, und in dieser Größenordnung ist skip und limit-Paginierung in Ordnung. Wenn ich öffentliche Registrierung anstelle von Einladungscodes benötige, tausche ich Cognito User Pools ein, die Autorisierungen kümmern sich nicht wirklich darum, es wird nur das JWT überprüfen.
Für bezahlte Events würde ich Stripe Checkout vor der Anmeldung platzieren. Ein Webhook feuert bei der Zahlung, der Backend erstellt den Gästedatensatz, sendet eine Bestätigung, die RSVP-Seite wird eine Ticket-Seite.
Wenn ich dies zu einer SaaS-Lösung mache, um mehrere Mandanten und Events zu verwalten, würde ich eine Mandanten-ID und eine Event-ID zum Datenmodell hinzufügen. Die Abfragen ein bisschen ändern und einige zusätzliche Funktionalitäten hinzufügen, aber der größte Teil der Logik ist da und erledigt.
Abschließende Worte
Ein kleines Hobbyprojekt, das mit „Ich möchte nicht in Gruppenchats nach RSVPs suchen“ begann, entwickelte sich zur Grundlage für eine vollständige serverlose Event-Plattform. MongoDB verwaltet die Daten, Lambda hält den Backend serverlos, und das PDP/PEP-Muster gibt mir die Autorisierung, die ich brauche.
Der Code ist verfügbar auf Serverless Handbook.
Schauen Sie sich meine anderen Beiträge auf jimmydqv.com an und folgen Sie mir auf LinkedIn für mehr Serverless-Inhalte.
Jetzt bauen Sie!