Push Notifications
This document describes the web push notification system implementation.
Overview
The application supports browser-based push notifications using the Web Push Protocol (RFC 8030) with VAPID authentication. This allows sending notifications to users even when they don't have the app open.
Architecture
Backend Components
WebPushDispatcher (
app/Services/Notifications/WebPushDispatcher.php)- Handles sending push notifications to user devices
- Uses the
minishlink/web-pushlibrary - Manages subscription lifecycle (creation, expiration, deletion)
- Implements automatic retry and error handling
NotificationObserver (
app/Observers/NotificationObserver.php)- Automatically triggers push notifications when in-app notifications are created
- Also broadcasts a real-time
NotificationCreatedevent (Echo/Reverb) for verified users - Read state sync is handled separately via a real-time
NotificationReadevent when users mark notifications as read - Only sends for notifications with
channel=in_app
PushSubscriptionController (
app/Http/Controllers/PushSubscriptionController.php)- REST API for managing push subscriptions
- Endpoints: list, create/update, delete
PushSubscription Model (
app/Models/PushSubscription.php)- Stores device subscriptions
- Fields: endpoint, keys (p256dh, auth), content_encoding, expires_at, last_seen_at
Frontend Components
Service Worker (
frontend/public/sw-notification-listeners.js)- Listens for push events from the browser
- Displays notifications using the Notifications API
- Handles notification clicks and navigation
- Manages subscription changes
NotificationPreferences Component (
frontend/src/components/notifications/NotificationPreferences.tsx)- UI for managing notification preferences (email, in-app, telegram per type)
- Includes
DeviceNotificationsCardfor web push setup andTelegramNotificationsCardfor Telegram linking - Handles permission requests
- Manages push subscription lifecycle
- Provides user feedback on subscription status
Web Push Utilities (
frontend/src/lib/web-push.ts)- Helper functions for encoding VAPID keys
- Service worker registration management
- Subscription serialization
Configuration
Environment Variables
Root .env (Docker Compose variables):
# Generate with: bun x web-push generate-vapid-keys
VAPID_PUBLIC_KEY=your_public_key
VAPID_PRIVATE_KEY=your_private_keyBackend backend/.env (Laravel runtime):
# These match the root .env values
VAPID_PUBLIC_KEY=your_public_key
VAPID_PRIVATE_KEY=your_private_key
VAPID_SUBJECT=mailto:your-email@example.com
# Optional: override default icon assets used in push payloads
APP_PUSH_ICON=/icon-192.png
APP_PUSH_BADGE=/icon-32.pngHow it works:
- The root
.envfile is read by Docker Compose, which passesVAPID_PUBLIC_KEYas a build argument to the Dockerfile - The Dockerfile sets both
VAPID_PUBLIC_KEYandVITE_VAPID_PUBLIC_KEYenvironment variables during the frontend build - The frontend Vite build bakes
VITE_VAPID_PUBLIC_KEYinto the JavaScript bundle - At runtime, the backend reads VAPID keys from
backend/.envvia Laravel's config system
Deployment: The deploy scripts handle this automatically - no manual exports needed!
Generating VAPID Keys
Automatic Generation (Recommended):
The setup script (utils/setup.sh) will automatically offer to generate VAPID keys during first-time setup:
./utils/deploy.shWhen prompted, choose "yes" to generate keys automatically. The script will:
- Check for Bun availability
- Generate keys using
bun x web-push generate-vapid-keys - Add them to both
.envandbackend/.env - Sync them automatically
Manual Generation:
If you prefer to generate keys manually:
bun x web-push generate-vapid-keysCopy the generated keys to both .env and backend/.env.
⚠️ Important: Never regenerate VAPID keys on an existing deployment with active users. Regenerating keys will invalidate all existing push subscriptions, and users will need to re-enable notifications.
Features
Payload Structure
Push notifications support the following fields:
{
"title": "Notification Title",
"body": "Notification message body",
"icon": "/icon-192.png",
"badge": "/icon-32.png",
"tag": "unique-notification-id",
"requireInteraction": false,
"data": {
"url": "/path/to/page",
"notification_id": "uuid",
"type": "notification_type",
"timestamp": 1234567890,
"app": {
"name": "Meo Mai Moi",
"icon": "/icon-192.png",
"badge": "/icon-32.png"
}
}
}Error Handling
The system handles various error scenarios:
- 410 Gone / 404 Not Found: Subscription expired, automatically removed
- 429 Too Many Requests: Rate limiting, subscription kept
- Network errors: Logged but subscription maintained
- Invalid subscriptions: Automatically cleaned up
Subscription Management
- Subscriptions are device-specific (one per browser/device)
- Automatically refreshed when expired
- Tracked with
last_seen_attimestamp for monitoring - Can be manually disabled by users
Usage
Testing Push Notifications
Use the artisan command:
php artisan push:test {user_id} --title="Test" --message="Hello"Sending from Code
use App\Services\Notifications\WebPushDispatcher;
use App\Models\User;
$user = User::find($userId);
$notification = Notification::find($notificationId);
app(WebPushDispatcher::class)->dispatch($user, $notification);User Subscription Flow
- User visits notification preferences page
- Clicks "Enable device notifications"
- Browser prompts for permission
- On grant, service worker subscribes to push
- Subscription saved to backend
- User receives notifications
Debugging
Enable logging in the browser console:
// In browser DevTools console
localStorage.setItem("debug", "notifications:*");Check Laravel logs for backend issues:
tail -f storage/logs/laravel.log | grep pushBrowser Compatibility
Push notifications work in:
- Chrome 50+
- Firefox 44+
- Edge 17+
- Safari 16+ (macOS Ventura+)
- Opera 37+
Not supported:
- iOS Safari (mobile)
- IE 11
Embedded in-app browsers
Push support in embedded app browsers/webviews is inconsistent and often unavailable, even when the same device supports push in a full browser.
Known problematic environments:
- Instagram in-app browser
- Facebook in-app browser
- Telegram Mini App / Telegram webviews (many clients)
Current frontend behavior (DeviceNotificationsCard):
- Uses capability checks first (
Notification,serviceWorker,PushManager) - Detects Instagram/Facebook in-app browsers and shows a targeted warning with "open in Safari/Chrome" guidance + copy-link action
- Detects Telegram Mini App context and shows a targeted hint to use Telegram notifications instead of web push
- Uses contextual error messages instead of generic "old browser" language
Security
- Uses VAPID authentication for secure message delivery
- Subscriptions are tied to specific users
- Endpoint URLs are hashed for privacy
- Keys stored encrypted in database
- Subject must be valid mailto: or https: URL
Limitations
- Notifications require user permission
- iOS Safari does not support web push (yet)
- Payload size limited to ~4KB
- Rate limits vary by browser vendor
- Users can revoke permission at any time
- Embedded browsers/webviews may block service workers, push subscription, or permission prompts
Troubleshooting
Notifications not appearing
- Check VAPID keys are configured correctly
- Verify service worker is registered
- Check browser notification permission
- Look for errors in browser console
- Check Laravel logs for backend errors
Subscription failures
- Ensure HTTPS (required for production)
- Check service worker scope
- Verify VAPID public key matches
- Check for expired subscriptions
- Confirm user is not inside an embedded browser (Instagram/Facebook/Telegram webview)
"Service worker not ready"
- Wait for page to fully load
- Reload the page
- Clear service worker cache
- Check for JavaScript errors
Future Improvements
- [ ] Add notification batching for multiple messages
- [ ] Implement notification delivery tracking
- [ ] Add notification action buttons
- [ ] Support notification images
- [ ] Add silent notifications for background sync
- [ ] Implement notification grouping
- [ ] Add delivery reports/analytics
- [ ] Support for notification sounds