Pagbuo ng Maaasahan, Timezone-Aware na Sistema ng Push Notification
Isang app para sa kalusugan at kagalingan ang nangailangan na magpadala ng personalized na pang-araw-araw na paalala — mga paalala sa pagkain, pag-check-in sa mood, mga prompt para sa hydration, mga signal para sa pagtulog, at mga custom na paalala — sa libu-libong user sa buong mundo. Ang hamon: bawat notification ay kailangang dumating sa tamang lokal na oras, eksaktong isang beses, at hindi kailanman sa isang hindi aktibong device. Dinisenyo at binuo namin ang distributed na pipeline na nagsasakatuparan nito.
Ang Hamon
- Katumpakan ng Timezone sa malaking sukat. Ang isang "9 AM breakfast reminder" ay may iba't ibang kahulugan para sa bawat user. Ang pagpapadala sa server time ay magpapadala ng notifikasyon sa isang tao sa Sydney nang 3 AM. Bawat notifikasyon ay kailangang tumugma sa lokal na oras ng user.
- Walang spam, walang pagdodoble. Ang pag-iskedyul ng mga cron ay tiyak na magkakapatong at muling tatakbo. Kung walang mahigpit na garantiya, ang isang user ay maaaring makatanggap ng parehong "oras ng tanghalian 🥗" na paalala nang dalawa o tatlong beses — isang mabilis na paraan para i-uninstall ang app.
- Hindi maaasahan ang mga Device. Ang mga user ay nag-u-uninstall ng apps, nagbabawi ng permissions, at patuloy na nagpapalit ng push tokens. Ang pagpapadala ng notifikasyon nang walang patumangga sa lumang tokens ay nag-aaksaya ng resources at sumisira sa delivery metrics.
- Tumpak na pag-o-oras nang walang brute-force na scheduler. Ang paghahatid ng daan-daang notifikasyon sa eksaktong minuto — nang walang cron na bumubomba sa database bawat 60 segundo — ay nangailangan ng mas matalinong mekanismo kaysa sa naive polling.
Ang Aming Solusyon
Binuo namin ang isang three-stage na pipeline na malinaw na naghihiwalay sa kung ano ang ipapadala, kung kailan ipapadala ito, at ang aktuwal na pagpapadala nito — upang ang bawat yugto ay maaaring bumagsak at makabawi nang independiyente. Ang database ang pinagmulan ng katotohanan, isang message queue ang humahawak sa tumpak na pag-o-oras, at isang worker layer ang nakikipag-ugnayan sa push provider.

Arkitektura
- Ang Expo-notifications ay isang React Native client na may native channels, natatanging tunog, at deep links na gumagamit ng iisang token format at delivery API para sa iOS at Android.
- NestJS backend na may expo-server-sdk bilang pinag-isang push abstraction sa ibabaw ng FCM at APNs.
- MongoDB bilang source of truth — mga koleksyon ng NotificationMessage, NotificationToken, at NotificationCounter.
- ActiveMQ (STOMP) delay queues, isa bawat kategorya (meal, mood, activity, safety, reminders), para sa tumpak na scheduled delivery.
- Creator crons na bumubuo ng timezone-resolved na mga talaan ng notification bawat user.
- Consumer workers na nagsu-subscribe sa bawat queue at nagsasagawa ng huling pagpapatunay bago magpadala.
- AWS ECS Fargate na nagpapatakbo ng crons at consumers; ActiveMQ sa isang dedicated na EC2 instance.
Mga Pangunahing Tampok
- Timezone-aware na pag-iiskedyul. Gamit ang date-fns-tz, ang lokal na oras ng pagpapadala ng bawat user ay kinokompyut, kinokonberte pabalik sa UTC para sa storage, at binabakuran ng UTC date windows upang garantiyahan ang isang paalala bawat araw.
- Idempotency na ipinapatupad ng database. Ang isang partial unique index sa mga pending na mensahe ay ginagawang imposible ang paglikha ng duplicate — kahit na tumakbo nang dalawang beses ang isang cron:
| // 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. Tumpak na paghahatid sa pamamagitan ng delay queues. Sa halip na per-minuto na cron, ang scheduler ay nag-e-enqueue ng mga mensahe ngayon ngunit ipinagpapaliban ang paghahatid sa eksaktong minutong nakatakda gamit ang scheduled-delay header ng ActiveMQ:
| client.send(`/queue/${queueName}`, { persistent: 'true' 'AMQ_SCHEDULED_DELAY': String(delayMs), // delivered exactly when due }, JSON.stringify(message)); |
4. Isang aktibong token bawat device. Ang isang partial unique index ay naggarantiya ng eksaktong isang aktibong token bawat device; ang mga bagong login ay maayos na nagre-retire ng lumang token, na may exponential-backoff retries upang makaligtas sa sabay-sabay na pag-sign-in.
5. Pag-check ng resibo + auto-cleanup. Pagkatapos magpadala, binobomba namin ang mga resibo ng Expo. Ang isang DeviceNotRegistered na tugon ay agad na nagde-deactivate sa hindi aktibong token upang hindi na kami mag-aksaya ng pagpapadala dito muli.
| f (receipt.status === 'error' && receipt.details?.error === 'DeviceNotRegistered') { await this.deactivateToken(token); // stop sending to dead devices } |
6. Failure ceiling. Bawat device ay may retry counter; pagkatapos ng 3 magkakasunod na pagkabigo, awtomatikong ire-retire ang token — walang infinite loops, walang zombie tokens.
7. Paggalang sa kagustuhan ng user sa oras ng pagpapadala. Ang kagustuhan sa notification ay muling chine-check ng consumer sa paghahatid, hindi lang sa pag-iiskedyul — kaya ang user na nag-opt out isang oras bago ang isang paalala ay hindi ito matatanggap. Ang mga mensahe ay nagre-resolve sa tapat na terminal states: success, failed, o is_missed.
Mga Resulta
- Bawat user ay nakakatanggap ng mga paalala sa tamang lokal na oras, sa buong mundo — walang off-hours na notification.
- Ganap na naalis ang duplicate na notification sa pamamagitan ng database-level na idempotency.
- Awtomatikong nade-detect at nire-retire ang mga hindi aktibo at lumang device tokens, pinapanatiling malinis ang delivery.
- Tumaas ang pagiging maaasahan at nabawasan ang load sa database sa pamamagitan ng tumpak, minuto-eksaktong paghahatid nang walang brute-force na scheduler.
Technology Stack
React Native · Expo Notifications · NestJS · TypeScript · MongoDB · ActiveMQ (STOMP) · expo-server-sdk · date-fns-tz · AWS ECS Fargate

