Opbygning af et pålideligt, tidszonebevidst push-notifikationssystem
En sundheds- og wellness-app skulle sende personlige daglige påmindelser – måltidspåmindelser, humør-tjek ind, hydreringstips, søvnsignaler og brugerdefinerede påmindelser – til tusindvis af brugere over hele kloden. Udfordringen: hver notifikation skulle ankomme på det rette lokale tidspunkt, præcis én gang og aldrig til en inaktiv enhed. Vi designede og byggede den distribuerede pipeline, der gør dette muligt.
Udfordringen
- Tidszonekorrekthed i stor skala. En "morgenmadspåmindelse kl. 9" betyder noget forskelligt for hver bruger. At sende i servertid ville pinge nogen i Sydney kl. 3 om morgenen. Hver notifikation skulle løses til brugerens lokale tidspunkt.
- Ingen spam, ingen duplikering. Planlægning af crons overlapper og genkører uundgåeligt. Uden strenge garantier kunne en enkelt bruger modtage den samme "tid til frokost 🥗"-påmindelse to eller tre gange – en hurtig vej til en afinstallation.
- Enheder er upålidelige. Brugere afinstallerer apps, tilbagekalder tilladelser og roterer push-tokens konstant. At affyre notifikationer blindt mod forældede tokens spilder ressourcer og korrumperer leveringsmetrikker.
- Præcis timing uden en brute-force scheduler. At levere hundredvis af notifikationer på præcise minutter – uden at en cron hamrer på databasen hvert 60. sekund – krævede en smartere mekanisme end naiv polling.
Vores løsning
Vi byggede en tre-trins pipeline, der klart adskiller hvad der skal sendes, hvornår det skal sendes og faktisk sender det – så hvert trin kan fejle og genoprette uafhængigt. Databasen er kilden til sandhed, en meddelelseskø håndterer præcis timing, og et enkelt worker-lag kommunikerer med push-udbyderen.

Arkitektur
- Expo-notifications er en React Native-klient med native kanaler, unikke lyde og deep links, der bruger et enkelt token-format og leverings-API til både iOS og Android.
- NestJS backend med expo-server-sdk som den forenede push-abstraktion over FCM og APNs.
- MongoDB som kilden til sandhed – NotificationMessage, NotificationToken og NotificationCounter-kollektioner.
- ActiveMQ (STOMP) forsinkelses-køer, én per kategori (måltid, humør, aktivitet, sikkerhed, påmindelser), for præcis planlagt levering.
- Creator crons, der genererer tidszone-opløste notifikationsposter per bruger.
- Consumer workers, der abonnerer på hver kø og udfører endelig validering, før de sender.
- AWS ECS Fargate kører crons og consumers; ActiveMQ på en dedikeret EC2-instans.
Nøglefunktioner
- Tidszonebevidst planlægning. Ved brug af date-fns-tz beregnes hver brugers lokale sendetid, konverteres tilbage til UTC til lagring, og afgrænses af UTC-datointervaller for at garantere én påmindelse per dag.
- Database-håndhævet idempotens. Et delvist unikt indeks på afventende meddelelser gør duplikering umulig – selv når en cron kører to gange:
| // Unik kun mens meddelelsen stadig er PENDING og ikke slettet schema.index( { userId: 1, notificationTokenId: 1, category: 1, label: 1, scheduledAt: 1 }, { unique: true, partialFilterExpression: { status: 'pending', isDeleted: false } } ); |
3. Præcis levering via forsinkelses-køer. I stedet for en cron per minut, køer scheduler'en meddelelser nu, men udskyder levering til det nøjagtige tidspunkt ved hjælp af ActiveMQs scheduled-delay header:
| client.send(`/queue/${queueName}`, { persistent: 'true', 'AMQ_SCHEDULED_DELAY': String(delayMs), // leveres præcis når den skal }, JSON.stringify(message)); |
4. Et enkelt aktivt token per enhed. Et delvist unikt indeks garanterer præcis ét aktivt token per enhed; nye logins pensionerer rent det gamle token, med exponential-backoff-forsøg for at overleve samtidige logins.
5. Kvitteringstjek + auto-oprydning. Efter afsendelse poller vi Expo-kvitteringer. En DeviceNotRegistered-respons deaktiverer øjeblikkeligt det døde token, så vi aldrig spilder en afsendelse på det igen.
| f (receipt.status === 'error' && receipt.details?.error === 'DeviceNotRegistered') { await this.deactivateToken(token); // stop med at sende til døde enheder } |
6. Fejl-loft. Hver enhed har en genforsøgscounter; efter 3 på hinanden følgende fejl pensioneres tokenet automatisk – ingen uendelige loops, ingen zombie-tokens.
7. Respektering af brugerens intention på afsendelsestidspunktet. Notifikationspræference kontrolleres igen af consumer'en ved levering, ikke kun ved planlægning – så en bruger, der fravælger en time før en påmindelse, modtager den aldrig. Meddelelser løses til ærlige terminaltilstande: success, failed, eller is_missed.
Resultater
- Hver bruger modtager påmindelser på det korrekte lokale tidspunkt, over hele verden – nul notifikationer uden for åbningstiderne.
- Duplikerede notifikationer elimineres fuldstændigt gennem idempotens på databaseniveau.
- Døde og forældede enheds-tokens detekteres og pensioneres automatisk, hvilket holder leveringen ren.
- Pålideligheden øges og databasebelastningen reduceres med præcis, minutnøjagtig levering uden en brute-force scheduler.
Teknologistak
React Native · Expo Notifications · NestJS · TypeScript · MongoDB · ActiveMQ (STOMP) · expo-server-sdk · date-fns-tz · AWS ECS Fargate

