Push Notifications
Klaxon pushes through two channels, both backed by Firebase:
- FCM — Android (GCM) and iOS (APNs) for the Expo mobile app
- Web Push — browser-registered subscriptions for the web dashboard when the tab is backgrounded
Both share the same notification_queue outbox table and the same worker; they differ only in the delivery adapter.
How it works
- Client (mobile or browser) requests notification permission and receives a device/subscription token from Firebase.
- Client registers the token:
POST /api/push/registerwithplatform: "fcm" | "webpush". - When an item is created, the server enqueues a push notification row in
notification_queuefor every subscriber in the org that hasn't opted out (per-channel + per-level rules, quiet hours, etc.). - The background worker picks up queue entries (
FOR UPDATE SKIP LOCKED) and calls the right adapter: FCM v1 HTTP API for mobile, Web Push for browsers. - Firebase delivers to the device.
Device registration
# Mobile (FCM)
curl -X POST http://localhost:3000/api/push/register \
-H "Authorization: Bearer TOKEN" \
-d '{"device_token": "fcm-token-here", "platform": "fcm"}'
# Browser (Web Push)
curl -X POST http://localhost:3000/api/push/register \
-H "Authorization: Bearer TOKEN" \
-d '{
"device_token": "<web-push-subscription-json>",
"platform": "webpush"
}'
# Unregister (either platform)
curl -X POST http://localhost:3000/api/push/unregister \
-H "Authorization: Bearer TOKEN" \
-d '{"device_token": "token-or-subscription"}'The web dashboard handles registration automatically via the Firebase Web SDK + a service worker (/firebase-messaging-sw.js). Browsers that don't grant notification permission fall back to WebSocket-only real-time updates.
Delivery Guarantees
The notification queue uses a persistent outbox pattern in PostgreSQL:
- At-least-once delivery: notifications survive server restarts
- Exponential backoff: failed deliveries retry with
2^attemptssecond delays - Max 5 attempts: after 5 failures, the notification is marked as permanently failed
- Concurrent-safe: workers use
FOR UPDATE SKIP LOCKEDfor safe multi-replica processing
Per-User Quiet Hours
Users can configure quiet hours to suppress push during specific times:
| Setting | Description |
|---|---|
quiet_hours_enabled | Enable/disable |
quiet_hours_start | Start time (HH:MM, e.g. "22:00") |
quiet_hours_end | End time (e.g. "07:00") — handles midnight wrapping |
quiet_hours_allow_levels | Comma-separated levels that bypass quiet hours (e.g. "error") |
Configuration
| Env var | Purpose |
|---|---|
FCM_SERVICE_ACCOUNT_JSON | Full contents of a Firebase service-account key (JSON). Used to mint OAuth2 tokens for the FCM v1 API. |
FIREBASE_VAPID_KEY | VAPID public key from Firebase Console → Cloud Messaging → Web Push certificates. Used by the browser SDK. Public — safe to ship to the client. |
FIREBASE_API_KEY, FIREBASE_PROJECT_ID, FIREBASE_APP_ID, FIREBASE_MESSAGING_SENDER_ID | Standard Firebase Web SDK config. All public client-side values. |
Without a valid service account, the worker logs an error per queue entry and marks them as permanently failed (so they don't retry forever). Use test mode Firebase credentials for local development, or leave them unset to disable push entirely.