Real-time Notifications — Realtime

Real-time Notifications Notification system: in-app toast, bell icon counter, push notification ke device. Tiga channel berbeda, satu trigger. Architecture…

Notifikasi realtime memberi tahu satu orang tentang kejadian yang menyangkut dirinya, saat itu juga, lewat jalan yang masih bisa sampai. Bayangin Bu Rini mau mengabari Pak Darto bahwa cucian #12 siap diambil. Kalau Pak Darto ada di toko, cukup dipanggil (notifikasi in-app). Kalau ia di rumah dengan HP di saku, Bu Rini titip ke operator yang bisa membunyikan HP-nya walaupun aplikasinya tertutup (Web Push). Kalau itu juga gak bisa, kirim WhatsApp: berbayar, tapi pasti terbaca. Dan semua kabar dicatat di buku, supaya bisa dibaca belakangan (pusat notifikasi).

🧪 Tebakan cepat: Pak Darto membuka aplikasi laundry di iPhone lewat tab Safari biasa, belum ditambahkan ke layar utama. Bisakah ia menerima Web Push setelah tab-nya ditutup? Jawabannya ada di bagian "Web Push: kabar saat aplikasi tertutup".

Satu kejadian, beberapa jalan

Saat Mas Eko menandai #12 Siap diambil, server Laravel memanggil server realtime:

async function kabari(userId, data) {
  const notif = await db.notifikasi.simpan({ userId, ...data, dibaca: false }) // 1. simpan dulu
  const online = io.of('/').adapter.rooms.get(`user:${userId}`)?.size > 0
  if (online) return io.to(`user:${userId}`).emit('notif:new', notif)      // 2. in-app
  for (const sub of await db.langganan.milik(userId)) {                      // 3. Web Push
    await kirimPush(sub, { title: data.title, url: `/pesanan/${data.order}` })
  }
}
Jalan Sampai kalau Butuh Biaya
In-app (socket) aplikasi sedang terbuka gak ada gratis
Web Push aplikasi tertutup juga izin, service worker, langganan gratis
WhatsApp atau SMS kapan pun, tanpa aplikasi nomor HP, akun bisnis bayar per pesan

Web Push: kabar saat aplikasi tertutup

  1. Pak Darto menekan tombol "Kabari saya", dan browser bertanya izin (Notification.requestPermission()).
  2. Halaman mendaftarkan service worker (/sw.js): skrip yang bisa dibangunkan browser walaupun tab-nya tertutup.
  3. pushManager.subscribe(...) meminta alamat ke push service milik browser (FCM untuk Chrome, Mozilla untuk Firefox, Apple untuk Safari). Hasilnya subscription: endpoint (sebuah URL) dan dua kunci enkripsi.
  4. Subscription dikirim ke server dan disimpan per perangkat.
  5. Untuk mengabari, server mengenkripsi isinya dengan kunci itu, menandatanganinya dengan kunci privat VAPID (bukti bahwa pengirimnya pemilik kunci publik yang dipakai saat subscribe), lalu POST ke endpoint. Push service menjawab 201 dan mengantar ke HP begitu HP online.
  6. Browser membangunkan service worker, yang wajib menampilkan notifikasi.
// halaman: di dalam handler klik tombol "Kabari saya"
const izin = await Notification.requestPermission() // 'granted', 'denied', atau 'default'
if (izin === 'granted') {
  const reg = await navigator.serviceWorker.register('/sw.js')
  const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VAPID_PUBLIC_KEY })
  await fetch('/api/push/langganan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sub) })
}

// sw.js
self.addEventListener('push', (event) => {
  const data = event.data.json()
  event.waitUntil(self.registration.showNotification(data.title, { data: { url: data.url } }))
})
self.addEventListener('notificationclick', (event) => {
  event.notification.close()
  event.waitUntil(clients.openWindow(event.notification.data.url))
})

Kapan minta izin

WhatsApp dan SMS

Sampai ke siapa pun yang punya nomor, tanpa aplikasi dan tanpa izin browser, dan pelanggan laundry di Indonesia memang membaca WhatsApp. Tapi bayar per pesan, dan pesan WhatsApp yang dimulai bisnis harus memakai template yang disetujui lebih dulu. Pakai untuk kabar penting saja (siap diambil), atau sebagai cadangan kalau notifikasi gak terbaca dalam 30 menit.

Pusat notifikasi dan angka belum dibaca

Angka di ikon lonceng harus tetap benar setelah reload, reconnect, atau dibaca dari perangkat lain. Jadi sumbernya database, dan socket cuma memperbaruinya di antara dua pengambilan:

let belumDibaca = new Map() // id -> notifikasi

socket.on('connect', async () => {
  const daftar = await fetch('/api/notifikasi?belum-dibaca=1').then((r) => r.json())
  belumDibaca = new Map(daftar.map((n) => [n.id, n])) // tiap tersambung: ambil ulang
  gambarLonceng()
})
socket.on('notif:new', (n) => { belumDibaca.set(n.id, n); gambarLonceng() })   // id sama: ditimpa
socket.on('notif:read', ({ id }) => { belumDibaca.delete(id); gambarLonceng() }) // dibaca di HP lain

function gambarLonceng() {
  lonceng.textContent = belumDibaca.size // dihitung dari isi, bukan +1 dan -1
}

⚠️ Jebakan yang sering ketemu

TL;DR: Simpan notifikasi ke database dulu, lalu kirim ke room pribadi user:darto (cara paling efisien untuk satu user) kalau ia online, atau lewat Web Push kalau aplikasinya tertutup. Web Push butuh izin (diminta dari tombol, bukan saat halaman dibuka; denied itu permanen), service worker, dan subscription dari push service browser; server mengirim isi terenkripsi bertanda tangan VAPID ke endpoint, dan service worker wajib memanggil showNotification. Di iOS 16.4 ke atas, Web Push cuma untuk web app di layar utama. WhatsApp dan SMS sampai ke siapa pun tapi berbayar. Angka belum dibaca dihitung dari daftar per id yang diambil ulang dari server setiap tersambung, bukan dari penghitung lokal.

Yang akan kamu pelajari