Erstellen eines zuverlässigen, zeitzonenbewussten Push-Benachrichtigungssystems
Eine Gesundheits- und Wellness-App musste personalisierte tägliche Anstöße – Mahlzeitenerinnerungen, Stimmungserfassungen, Hydrationsaufforderungen, Schlafhinweise und benutzerdefinierte Erinnerungen – an Tausende von Benutzern weltweit senden. Der Haken: Jede Benachrichtigung musste zur richtigen Ortszeit, genau einmal und niemals an ein inaktives Gerät zugestellt werden. Wir haben die verteilte Pipeline entworfen und gebaut, die dies ermöglicht.
Die Herausforderung
- Zeitzonenkorrektheit im großen Maßstab. Eine „9 Uhr Frühstückserinnerung“ bedeutet für jeden Benutzer etwas anderes. Das Senden in Serverzeit würde jemanden in Sydney um 3 AM pingen. Jede Benachrichtigung musste sich auf den lokalen Zeitpunkt des Benutzers beziehen.
- Kein Spam, keine Duplikation. Das Planen von crons führt unweigerlich zu Überschneidungen und Wiederholungen. Ohne strenge Garantien könnte ein einzelner Benutzer denselben „Zeit fürs Mittagessen 🥗“-Anstoß zwei- oder dreimal erhalten – ein schneller Weg zur Deinstallation.
- Geräte sind unzuverlässig. Benutzer deinstallieren Apps, widerrufen Berechtigungen und rotieren push tokens ständig. Das blinde Senden von Benachrichtigungen an veraltete tokens verschwendet Ressourcen und verfälscht die Zustellmetriken.
- Präzises Timing ohne Brute-Force-Scheduler. Das Zustellen Hunderter von Benachrichtigungen zu exakten Minuten – ohne dass ein cron die Datenbank alle 60 Sekunden bombardiert – erforderte einen intelligenteren Mechanismus als naives Polling.
Unsere Lösung
Wir haben eine dreistufige Pipeline entwickelt, die was gesendet werden soll, wann es gesendet werden soll und das eigentliche Senden sauber trennt – sodass jede Stufe unabhängig voneinander ausfallen und wiederhergestellt werden kann. Die Datenbank ist die Quelle der Wahrheit, eine Message Queue handhabt das präzise Timing, und eine einzelne Worker-Schicht kommuniziert mit dem push provider.

Architektur
- Expo-notifications ist ein React Native client mit nativen Kanälen, einzigartigen Geräuschen und Deep Links, das ein einziges token-Format und eine Delivery API für iOS und Android verwendet.
- NestJS backend mit expo-server-sdk als vereinheitlichter push abstraction ĂĽber FCM und APNs.
- MongoDB als Quelle der Wahrheit — NotificationMessage, NotificationToken, und NotificationCounter collections.
- ActiveMQ (STOMP) delay queues, eine pro Kategorie (Mahlzeit, Stimmung, Aktivität, Sicherheit, Erinnerungen), für präzise geplante Zustellung.
- Creator crons, die zeitzonenaufgelöste Benachrichtigungsdatensätze pro Benutzer generieren.
- Consumer workers, die jede queue abonnieren und die endgĂĽltige Validierung vor dem Senden durchfĂĽhren.
- AWS ECS Fargate, auf dem crons und consumers laufen; ActiveMQ auf einer dedizierten EC2 instance.
Hauptmerkmale
- Zeitzonenbewusste Planung. Mithilfe von date-fns-tz wird die lokale Sendezeit jedes Benutzers berechnet, zur Speicherung in UTC zurĂĽckkonvertiert und durch UTC-Datumsfenster begrenzt, um einen AnstoĂź pro Tag zu garantieren.
- Datenbank-erzwungene Idempotenz. Ein partieller eindeutiger Index für ausstehende Nachrichten macht die Duplikaterstellung unmöglich – selbst wenn ein cron zweimal läuft:
| // Unique only while the message is still PENDING and not deleted schema.index( { userId: 1, notificationTokenId: 1, category: 1, label: 1, scheduledAt: 1 }, { unique: true, partialFilterExpression: { status: 'pending', isDeleted: false } } ); |
3. Präzise Zustellung über delay queues. Anstelle eines minütlichen crons reiht der Scheduler Nachrichten jetzt ein, verzögert die Zustellung jedoch bis zur exakten Fälligkeitsminute mithilfe des ActiveMQ's scheduled-delay header:
| client.send(`/queue/${queueName}`, { persistent: 'true' 'AMQ_SCHEDULED_DELAY': String(delayMs), // delivered exactly when due }, JSON.stringify(message)); |
4. Einziger aktiver token pro Gerät. Ein partieller eindeutiger Index garantiert genau einen aktiven token pro Gerät; neue Logins setzen den alten token sauber außer Kraft, mit exponential-backoff retries, um gleichzeitige Anmeldungen zu überstehen.
5. Empfangsbestätigungsprüfung + automatische Bereinigung. Nach dem Senden rufen wir Expo receipts ab. Eine DeviceNotRegistered Antwort deaktiviert sofort den inaktiven token, sodass wir nie wieder eine Sendung dafür verschwenden.
| f (receipt.status === 'error' && receipt.details?.error === 'DeviceNotRegistered') { await this.deactivateToken(token); // stop sending to dead devices } |
6. Fehlerschwelle. Jedes Gerät hat einen retry counter; nach 3 aufeinanderfolgenden Fehlern wird der token automatisch außer Betrieb genommen – keine Endlosschleifen, keine zombie tokens.
7. Respektierung der Benutzerabsicht zum Sendezeitpunkt. Die Benachrichtigungseinstellung wird vom consumer bei der Zustellung erneut überprüft, nicht nur bei der Planung – so erhält ein Benutzer, der sich eine Stunde vor einem Anstoß abmeldet, diesen niemals. Nachrichten lösen sich in ehrliche Endzustände auf: success, failed, oder is_missed.
Ergebnisse
- Jeder Benutzer erhält Anstöße zur korrekten Ortszeit, weltweit – null Benachrichtigungen außerhalb der Geschäftszeiten.
- Duplikate von Benachrichtigungen wurden durch Idempotenz auf Datenbankebene vollständig eliminiert.
- Inaktive und veraltete device tokens werden automatisch erkannt und stillgelegt, um die Zustellung sauber zu halten.
- Die Zuverlässigkeit wird erhöht und die Datenbanklast reduziert durch präzise, minutengenaue Zustellung ohne Brute-Force-Scheduler.
Technologie-Stack
React Native · Expo Notifications · NestJS · TypeScript · MongoDB · ActiveMQ (STOMP) · expo-server-sdk · date-fns-tz · AWS ECS Fargate

