# Web Push Notifications The `@xtr-dev/payload-notifications` plugin includes built-in support for web push notifications for PWA and mobile browser users. ## Features - 🔔 Web Push Notifications for mobile PWA support - 📲 Service Worker integration for offline notifications - 🔐 VAPID keys support for secure push messaging - 🎯 Auto-push on notification creation - 🔄 Custom notification transformers - 👤 Anonymous user notification support ## Setup VAPID Keys **Step 1:** Generate VAPID keys for secure push messaging: ```bash npx web-push generate-vapid-keys ``` This will output something like: ``` ======================================= Public Key: BNde-uFUkQB5BweFbOt_40Tn3xZahMop2JKT8kqRn4UqMMinieguHmVCTxwN_qfM-jZ0YFpVpIk3CWehlXcTl8A Private Key: RVtnLcW8qlSkuhNskz8lwBwYcam78x-zO0Ssm_P2bmE ======================================= ``` **Step 2:** Add the keys to your environment variables: ```env VAPID_PUBLIC_KEY=BNde-uFUkQB5BweFbOt_40Tn3xZahMop2JKT8kqRn4UqMMinieguHmVCTxwN_qfM-jZ0YFpVpIk3CWehlXcTl8A VAPID_PRIVATE_KEY=RVtnLcW8qlSkuhNskz8lwBwYcam78x-zO0Ssm_P2bmE ``` **Step 3:** Restart your application to load the new environment variables. ⚠️ **Important:** Keep your private key secure and never commit it to version control! ## Plugin Configuration ### Basic Web Push Setup ```typescript import { notificationsPlugin } from '@xtr-dev/payload-notifications' notificationsPlugin({ webPush: { enabled: true, autoPush: true, // Automatically send push notifications when notifications are created vapidPublicKey: process.env.VAPID_PUBLIC_KEY, vapidPrivateKey: process.env.VAPID_PRIVATE_KEY, vapidSubject: 'mailto:your-email@example.com', } }) ``` ### Advanced Configuration ```typescript notificationsPlugin({ webPush: { enabled: true, autoPush: true, vapidPublicKey: process.env.VAPID_PUBLIC_KEY, vapidPrivateKey: process.env.VAPID_PRIVATE_KEY, vapidSubject: 'mailto:your-email@example.com', // Optional: Custom notification transformer transformNotification: (notification) => ({ title: `🔔 ${notification.title}`, body: extractTextFromRichText(notification.message).substring(0, 120) + '...', icon: '/icons/notification-icon.png', badge: '/icons/notification-badge.png', data: { notificationId: notification.id, url: `/admin/collections/notifications/${notification.id}` }, actions: [ { action: 'view', title: 'View', icon: '/icons/view.png' }, { action: 'dismiss', title: 'Dismiss' } ] }), // Optional: Custom hook for finding push subscriptions (for anonymous notifications) findSubscriptions: async (notification, payload) => { // Custom logic to find subscriptions based on notification data // Return array of push subscription documents return [] } } }) ``` ## Anonymous Notifications Support For scenarios where you need to send notifications to anonymous users or have custom recipient logic (e.g., notifications based on email addresses, phone numbers, or custom identifiers), you can use the `findSubscriptions` hook combined with custom fields. ### Example: Email-based notifications ```typescript notificationsPlugin({ // Add custom email field to notifications collection fields: [ { name: 'recipientEmail', type: 'email', label: 'Recipient Email', admin: { description: 'Email address of the notification recipient', }, } ], webPush: { enabled: true, autoPush: true, vapidPublicKey: process.env.VAPID_PUBLIC_KEY, vapidPrivateKey: process.env.VAPID_PRIVATE_KEY, vapidSubject: 'mailto:your-email@example.com', // Custom hook to find subscriptions based on email findSubscriptions: async (notification, payload) => { if (!notification.recipientEmail) return [] // Find push subscriptions associated with this email const subscriptions = await payload.find({ collection: 'push-subscriptions', where: { and: [ { recipientEmail: { equals: notification.recipientEmail } }, { isActive: { equals: true } }, // Channel filtering (if specified) ...(notification.channel ? [{ or: [ { channels: { contains: notification.channel } }, { channels: { contains: 'all' } }, { channels: { exists: false } }, ] }] : []) ] } }) return subscriptions.docs } } }) ``` ### Example: Phone number-based notifications ```typescript notificationsPlugin({ fields: [ { name: 'recipientPhone', type: 'text', label: 'Recipient Phone', admin: { description: 'Phone number of the notification recipient', }, } ], webPush: { enabled: true, autoPush: true, vapidPublicKey: process.env.VAPID_PUBLIC_KEY, vapidPrivateKey: process.env.VAPID_PRIVATE_KEY, vapidSubject: 'mailto:your-email@example.com', findSubscriptions: async (notification, payload) => { if (!notification.recipientPhone) return [] // Custom logic to find subscriptions by phone number // You might have a separate mapping table or user lookup const user = await payload.find({ collection: 'users', where: { phone: { equals: notification.recipientPhone } }, limit: 1 }) if (!user.docs[0]) return [] const subscriptions = await payload.find({ collection: 'push-subscriptions', where: { and: [ { user: { equals: user.docs[0].id } }, { isActive: { equals: true } } ] } }) return subscriptions.docs } } }) ``` **Key Points:** - The default `recipient` field remains a user relationship for standard notifications - Add custom recipient fields via the `fields` option for your specific use case - Use the `findSubscriptions` hook to implement custom subscription lookup logic - The hook receives the full notification document and payload instance - Return an array of push subscription documents that should receive the notification - The plugin will handle the actual push notification sending and error handling ## Client-Side Integration ⚠️ **Authentication Required:** Users must be signed in to subscribe to push notifications. Push subscriptions are associated with user accounts. ### React Hook ```typescript import { usePushNotifications } from '@xtr-dev/payload-notifications/client' function NotificationSettings() { const { isSupported, isSubscribed, permission, subscribe, unsubscribe } = usePushNotifications(process.env.NEXT_PUBLIC_VAPID_KEY) if (!isSupported) return
Status: {isSubscribed ? 'Subscribed' : 'Not subscribed'}
Permission: {permission}
{!isSubscribed ? ( ) : ( )}