Notifications
Web Push (VAPID) subscriptions and delivery.
The backend sends web push notifications (VAPID) to browsers for new articles, videos, forum replies, and reminders.
Subscribe
POST /api/push/subscribe — body:
{
"endpoint": "https://fcm.googleapis.com/...",
"keys": { "p256dh": "...", "auth": "..." },
"user_id": 42
}Stored in push_subscriptions (endpoint unique, user_id nullable for anonymous). The frontend subscribes on HomeLoggedIn mount via push/registerPush.js (guarded by a push_registered localStorage flag).
Send
POST /api/push/send — body:
{
"user_id": 42,
"payload": { "title": "...", "body": "...", "url": "/docs/..." }
}Uses SherClockHolmes/webpush-go with the VAPID keys from env (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT).
When notifications fire
| Trigger | Channel |
|---|---|
| Reminder schedule match | WhatsApp + web push |
| Forum thread reply (not self-reply) | WhatsApp to thread creator |
| New forum thread | WhatsApp broadcast (preference-filtered) |
| New article | WhatsApp broadcast (randomized delay) |
| New video | WhatsApp broadcast (randomized delay) |
All notification dispatches run in goroutines so the triggering HTTP response is not blocked.
Service workers unregistered on frontend
The frontend unregisters service workers on app load (src/index.js) — this is cache-clearing behavior, not a PWA. Web Push still works because the PushManager subscription persists independently of the service worker lifecycle, but this is fragile and worth revisiting.
Notification service wiring
Cross-module notification is wired via setter injection in cmd/main.go:
forumService.SetNotificationService(whatsappService)artikelService.SetArticleNotificationService(whatsappService)pengingatService.SetPushNotificationService(pushNotificationService)
The contracts live in internal/shared/interface.go (NotificationService, PengingatNotificationService, ArticleNotificationService, YouTubeNotificationService), with NoOp* implementations for tests/default wiring.