17 KiB
Web Push Notifications Setup Guide
This guide explains how to implement and use Web Push notifications in the Dmail API. Web Push allows you to send notifications directly to users' browsers, even when your web app is not open.
Overview
The Web Push notification system sends real-time notifications to users' browsers when new emails arrive. It uses the standard Web Push API supported by all modern browsers and works with service workers.
Architecture
Email Received → EmailMonitoringService → NotificationQueue → NewEmailHandler → PushNotificationHandler → Browser Push Service → User's Browser
Setup Instructions
1. Generate VAPID Keys
VAPID (Voluntary Application Server Identification) keys are required for Web Push notifications:
# Generate VAPID keys using Node.js
node -e "
const webpush = require('web-push');
const vapidKeys = webpush.generateVAPIDKeys();
console.log('Public Key:', vapidKeys.publicKey);
console.log('Private Key:', vapidKeys.privateKey);
"
2. Configure Environment Variables
Add these environment variables to your .env file:
# Web Push Notifications Configuration
PUSH_NOTIFICATIONS_ENABLED=true
VAPID_PUBLIC_KEY=your_generated_public_key_here
VAPID_PRIVATE_KEY=your_generated_private_key_here
VAPID_SUBJECT=mailto:admin@yourdomain.com
# Optional: Push notification settings
PUSH_TTL=86400
PUSH_URGENCY=normal
PUSH_RETRY_ATTEMPTS=3
PUSH_RETRY_DELAY=5000
# Frontend URL for notification actions
FRONTEND_URL=https://mail.yourdomain.com
3. Install Dependencies
# Install web-push library
npm install web-push
# or
pnpm add web-push
4. Run Database Migration
# Apply the database migration for web push notification fields
psql -d your_database -f database/migrations/add_push_notifications_to_users.sql
5. Update App Module
Add the push notifications config to your app.module.ts:
import { pushNotificationsConfig } from "./configs/push-notifications.config";
@Module({
imports: [
ConfigModule.forRoot({
load: [
// ... other configs
pushNotificationsConfig,
],
}),
// ... other modules
],
})
export class AppModule {}
Frontend Implementation
1. Service Worker Setup
Create public/sw.js in your frontend project:
// Service Worker for handling push notifications
self.addEventListener("push", function (event) {
if (event.data) {
const data = event.data.json();
const options = {
body: data.body,
icon: data.icon || "/assets/email-icon.png",
badge: data.badge || "/assets/email-badge.png",
tag: data.tag || "email-notification",
requireInteraction: data.requireInteraction || false,
actions: data.actions || [],
data: data.data || {},
};
event.waitUntil(self.registration.showNotification(data.title, options));
}
});
// Handle notification clicks
self.addEventListener("notificationclick", function (event) {
event.notification.close();
if (event.action === "view") {
// Open the email in the app
event.waitUntil(clients.openWindow(event.notification.data.url || "/"));
} else if (event.action === "dismiss") {
// Just close the notification
return;
} else {
// Default action - open the app
event.waitUntil(clients.openWindow(event.notification.data.url || "/"));
}
});
2. Client-side JavaScript
Add this to your main JavaScript file:
class WebPushManager {
constructor(apiBaseUrl, getAuthToken) {
this.apiBaseUrl = apiBaseUrl;
this.getAuthToken = getAuthToken;
this.vapidPublicKey = null;
}
// Initialize and register service worker
async init() {
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
console.warn("Push messaging is not supported");
return false;
}
try {
// Register service worker
const registration = await navigator.serviceWorker.register("/sw.js");
console.log("Service Worker registered:", registration);
// Get VAPID public key
await this.getVAPIDKey();
return true;
} catch (error) {
console.error("Service Worker registration failed:", error);
return false;
}
}
// Get VAPID public key from server
async getVAPIDKey() {
try {
const response = await fetch(`${this.apiBaseUrl}/users/push-notifications/vapid-key`, {
headers: {
Authorization: `Bearer ${this.getAuthToken()}`,
},
});
if (response.ok) {
const data = await response.json();
this.vapidPublicKey = data.vapidPublicKey;
return this.vapidPublicKey;
}
} catch (error) {
console.error("Failed to get VAPID key:", error);
}
return null;
}
// Request permission and subscribe to push notifications
async subscribeToPush() {
if (!this.vapidPublicKey) {
await this.getVAPIDKey();
}
if (!this.vapidPublicKey) {
throw new Error("VAPID public key not available");
}
// Request permission
const permission = await Notification.requestPermission();
if (permission !== "granted") {
throw new Error("Push notification permission denied");
}
// Get service worker registration
const registration = await navigator.serviceWorker.ready;
try {
// Subscribe to push notifications
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey),
});
// Send subscription to server
await this.sendSubscriptionToServer(subscription);
console.log("Successfully subscribed to push notifications");
return subscription;
} catch (error) {
console.error("Failed to subscribe to push notifications:", error);
throw error;
}
}
// Send subscription to server
async sendSubscriptionToServer(subscription) {
const response = await fetch(`${this.apiBaseUrl}/users/push-subscriptions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.getAuthToken()}`,
},
body: JSON.stringify({
subscription: subscription.toJSON(),
deviceType: "web",
deviceName: navigator.userAgent.slice(0, 50),
}),
});
if (!response.ok) {
throw new Error("Failed to send subscription to server");
}
return response.json();
}
// Test push notification
async testNotification() {
const response = await fetch(`${this.apiBaseUrl}/users/push-notifications/test`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.getAuthToken()}`,
},
body: JSON.stringify({
message: "Test notification from Dmail Web App",
}),
});
if (!response.ok) {
throw new Error("Failed to send test notification");
}
return response.json();
}
// Check if user is subscribed
async isSubscribed() {
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
return false;
}
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
return !!subscription;
}
// Unsubscribe from push notifications
async unsubscribe() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
// Unsubscribe from browser
await subscription.unsubscribe();
// Remove from server
await fetch(`${this.apiBaseUrl}/users/push-subscriptions`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.getAuthToken()}`,
},
body: JSON.stringify({
endpoint: subscription.endpoint,
}),
});
console.log("Successfully unsubscribed from push notifications");
}
}
// Utility function to convert VAPID key
urlBase64ToUint8Array(base64String) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
}
// Usage example
const pushManager = new WebPushManager("https://api.yourdomain.com", () => localStorage.getItem("authToken"));
// Initialize on page load
pushManager.init().then((success) => {
if (success) {
console.log("Web Push initialized successfully");
}
});
// Export for use in your app
window.webPushManager = pushManager;
3. User Interface Integration
Add UI components to your app:
<!-- Notification permission UI -->
<div id="push-notification-ui" style="display: none;">
<div class="notification-prompt">
<h3>📬 Enable Email Notifications</h3>
<p>Get instant notifications when new emails arrive, even when the app is closed.</p>
<button id="enable-notifications" class="btn btn-primary">Enable Notifications</button>
<button id="test-notification" class="btn btn-secondary" style="display: none;">Test Notification</button>
<button id="disable-notifications" class="btn btn-danger" style="display: none;">Disable</button>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ui = document.getElementById("push-notification-ui");
const enableBtn = document.getElementById("enable-notifications");
const testBtn = document.getElementById("test-notification");
const disableBtn = document.getElementById("disable-notifications");
// Show UI if push is supported
if ("serviceWorker" in navigator && "PushManager" in window) {
ui.style.display = "block";
// Check current subscription status
const isSubscribed = await window.webPushManager.isSubscribed();
if (isSubscribed) {
enableBtn.style.display = "none";
testBtn.style.display = "inline-block";
disableBtn.style.display = "inline-block";
}
// Enable notifications
enableBtn.addEventListener("click", async () => {
try {
await window.webPushManager.subscribeToPush();
enableBtn.style.display = "none";
testBtn.style.display = "inline-block";
disableBtn.style.display = "inline-block";
alert("Push notifications enabled successfully!");
} catch (error) {
alert("Failed to enable notifications: " + error.message);
}
});
// Test notification
testBtn.addEventListener("click", async () => {
try {
await window.webPushManager.testNotification();
alert("Test notification sent!");
} catch (error) {
alert("Failed to send test notification: " + error.message);
}
});
// Disable notifications
disableBtn.addEventListener("click", async () => {
try {
await window.webPushManager.unsubscribe();
enableBtn.style.display = "inline-block";
testBtn.style.display = "none";
disableBtn.style.display = "none";
alert("Push notifications disabled");
} catch (error) {
alert("Failed to disable notifications: " + error.message);
}
});
}
});
</script>
API Endpoints
Push Subscription Management
POST /users/push-subscriptions- Register a new push subscriptionPATCH /users/push-subscriptions- Update push subscriptionsDELETE /users/push-subscriptions- Remove a push subscription
Push Notification Settings
PATCH /users/push-notifications/settings- Enable/disable push notificationsGET /users/push-notifications/info- Get push notification info and settingsPOST /users/push-notifications/test- Send a test notificationGET /users/push-notifications/vapid-key- Get VAPID public key
Usage Examples
Register Push Subscription
curl -X POST https://api.yourdomain.com/users/push-subscriptions \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subscription": {
"endpoint": "https://fcm.googleapis.com/fcm/send/...",
"keys": {
"p256dh": "BNbxxx...",
"auth": "abc123..."
}
},
"deviceType": "web",
"deviceName": "Chrome on MacBook Pro"
}'
Test Push Notification
curl -X POST https://api.yourdomain.com/users/push-notifications/test \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": "Test notification from Dmail API"
}'
Get VAPID Public Key
curl -X GET https://api.yourdomain.com/users/push-notifications/vapid-key \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
How It Works
- Service Worker Registration: Your web app registers a service worker that can receive push messages
- User Permission: The user grants permission for notifications
- Subscription Creation: The browser creates a push subscription with a unique endpoint
- Server Registration: The subscription is sent to your API and stored in the database
- Email Detection: When new emails arrive, the EmailMonitoringService detects them
- Notification Processing: The NewEmailHandler triggers the PushNotificationHandler
- Push Message: The server sends a push message to the browser's push service
- Notification Display: The service worker receives the message and shows the notification
Browser Support
Web Push is supported by:
- ✅ Chrome 50+
- ✅ Firefox 44+
- ✅ Safari 16.0+
- ✅ Edge 17+
- ✅ Opera 37+
Notification Flow
sequenceDiagram
participant U as User's Browser
participant SW as Service Worker
participant API as Dmail API
participant PS as Push Service
participant E as Email System
U->>SW: Register Service Worker
U->>API: Request VAPID key
API->>U: Return VAPID public key
U->>U: Request notification permission
U->>PS: Create push subscription
PS->>U: Return subscription object
U->>API: Register subscription
API->>API: Store subscription
E->>API: New email detected
API->>PS: Send push message
PS->>SW: Deliver push message
SW->>U: Show notification
Security Considerations
- VAPID Keys: Keep your private VAPID key secure and never expose it to the client
- Subscription Validation: Always validate subscription objects before storing them
- Rate Limiting: Implement rate limiting to prevent notification spam
- Data Privacy: Be mindful of what data you include in push notifications
- User Consent: Always request explicit user permission before subscribing to push notifications
Troubleshooting
Common Issues
-
Notifications not received:
- Check if the user has granted notification permission
- Verify the service worker is registered correctly
- Ensure VAPID keys are properly configured
- Check browser console for errors
-
VAPID key errors:
- Ensure VAPID keys are correctly generated and configured
- Verify the VAPID subject is a valid mailto: or https: URL
-
Service worker issues:
- Check if the service worker file is accessible
- Verify the service worker scope includes your web app
- Ensure HTTPS is used (required for service workers)
Debug Commands
# Test VAPID key configuration
curl -X GET https://api.yourdomain.com/users/push-notifications/vapid-key \
-H "Authorization: Bearer YOUR_TOKEN"
# Check user's notification settings
curl -X GET https://api.yourdomain.com/users/push-notifications/info \
-H "Authorization: Bearer YOUR_TOKEN"
# Send test notification
curl -X POST https://api.yourdomain.com/users/push-notifications/test \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message": "Test"}'
Monitoring and Analytics
- Subscription Management: Track active subscriptions per user
- Delivery Success: Monitor push notification delivery rates
- User Engagement: Track notification click-through rates
- Error Handling: Log and monitor push notification failures
Performance Considerations
- Payload Size: Keep push notification payloads small (max 4KB)
- Batch Processing: Process multiple subscriptions efficiently
- TTL Settings: Configure appropriate Time-To-Live for notifications
- Cleanup: Regularly remove invalid/expired subscriptions
Future Enhancements
- Rich Notifications: Add images and action buttons
- Notification Categories: Different notification types for different email categories
- User Preferences: Granular control over notification timing and content
- Analytics Dashboard: Web interface for monitoring push notification metrics
- A/B Testing: Test different notification strategies and content
Support
For issues and questions:
- Check browser developer console for errors
- Verify service worker registration and subscription
- Test VAPID key configuration
- Review application logs for error messages