Push Notifications
This guide covers how to set up and use push notifications in PWASK, including VAPID key generation, subscription management, and sending notifications from the backend.
Overview
PWASK includes a complete push notification implementation using the Web Push API. Push notifications allow you to re-engage users even when they're not actively using your app.
Features
- ✅ Browser permission request flow
- ✅ VAPID key authentication for secure delivery
- ✅ Subscription persistence in Supabase
- ✅ Service Worker integration with notification actions
- ✅ Background notification handling
- ✅ Localized notification content
- ✅ Demo page with test notifications
How It Works
- User grants permission: Browser prompts user to allow notifications
- Browser subscribes: Creates a unique push subscription endpoint
- Server stores subscription: Saved to Supabase
push_subscriptionstable - Server sends notification: Uses
web-pushlibrary to deliver payload - Service Worker receives: Displays notification with custom actions
- User interacts: Click opens the app at the specified URL
Setup Guide
Step 1: Generate VAPID Keys
VAPID (Voluntary Application Server Identification) keys authenticate your server when sending push notifications.
# Install web-push CLI globally (one-time)
npm install -g web-push
# Generate VAPID keys
web-push generate-vapid-keys
You'll see output like:
=======================================
Public Key:
BEl62iUYgUivxIkv69yViEuiBIa...
Private Key:
YFeDEb-h1pKe0Gf...
=======================================
Important: Store the private key securely. Never commit it to version control or expose it to the client.
Step 2: Configure Environment Variables
Add the VAPID keys to your .env.local file:
# Public key (safe to expose to the browser)
NEXT_PUBLIC_VAPID_PUBLIC_KEY=BEl62iUYgUivxIkv69yViEuiBIa...
# Private key (server-side only, never expose)
VAPID_PRIVATE_KEY=YFeDEb-h1pKe0Gf...
# Contact email for VAPID identification
VAPID_SUBJECT=mailto:your-email@example.com
For production deployments (Vercel, Netlify, etc.):
- Add these environment variables in your hosting provider's dashboard
- Ensure
VAPID_PRIVATE_KEYis marked as secret/sensitive - Redeploy your app after adding the variables
Step 3: Run Database Migration
The push notification feature requires a push_subscriptions table:
# Apply the migration
pnpm supabase db push
# Or manually run the migration SQL
pnpm supabase migration up
This creates:
push_subscriptionstable with columns:user_id,endpoint,p256dh_key,auth_key- Row-Level Security (RLS) policies ensuring users can only manage their own subscriptions
- Indexes for efficient subscription lookups
Step 4: Verify Setup
-
Start your development server:
pnpm dev -
Navigate to the demo page:
http://localhost:3000/en/demo/notifications -
Click "Enable Notifications" and grant permission when prompted
-
Click "Send Test Notification" to verify it works
If you see a notification appear, your setup is complete!
Using Push Notifications in Your App
Client-Side Implementation
Import the PushNotificationManager component anywhere in your app:
import PushNotificationManager from '@/components/PushNotificationManager';
export default function MyPage() {
return (
<div>
<h1>My App</h1>
<PushNotificationManager />
</div>
);
}
The component handles:
- Permission request UI
- Subscription management
- Browser compatibility checks
- VAPID key validation
- Error handling and user feedback
Programmatic Subscription
You can also manage subscriptions programmatically using the utilities:
import {
isPushSupported,
requestPermission,
subscribeToPush,
unsubscribeFromPush,
getPushSubscription,
} from '@/lib/notifications/push';
// Check if push is supported in the browser
if (isPushSupported()) {
// Request permission
const permission = await requestPermission();
if (permission === 'granted') {
// Subscribe to push notifications
const subscription = await subscribeToPush();
console.log('Subscribed:', subscription);
}
}
// Get current subscription
const currentSub = await getPushSubscription();
// Unsubscribe
const success = await unsubscribeFromPush();
Sending Notifications from the Backend
Use the /api/notifications/send endpoint to send notifications:
// Example: Send a notification when a new comment is added
async function notifyUser(userId: string, comment: Comment) {
const response = await fetch('/api/notifications/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'New Comment',
body: `${comment.author} replied to your post`,
icon: '/icons/icon-192x192.png',
badge: '/icons/icon-96x96.png',
url: `/posts/${comment.postId}#comment-${comment.id}`,
}),
});
return response.json();
}
API Endpoint: POST /api/notifications/send
Request Body:
{
"title": "Notification Title",
"body": "Notification message",
"icon": "/icons/icon-192x192.png",
"badge": "/icons/icon-96x96.png",
"url": "/target-page"
}
Response:
{
"success": true,
"sent": 2,
"failed": 0
}
The API will:
- Verify the user is authenticated
- Look up all push subscriptions for that user
- Send the notification to each subscription
- Return success/failure counts
Custom Notification Actions
The service worker includes default action buttons:
- Open: Opens the app at the notification's URL
- Close: Dismisses the notification
To customize actions, edit /src/sw.ts:
const options = {
body: notificationData.body,
icon: notificationData.icon,
badge: notificationData.badge,
tag: notificationData.tag,
requireInteraction: false,
data: notificationData.data,
actions: [
{
action: 'view',
title: 'View Details',
},
{
action: 'dismiss',
title: 'Dismiss',
},
],
} as NotificationOptions;
Then handle the actions in the notificationclick event:
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'view') {
// Custom action logic
const url = event.notification.data?.url || '/';
event.waitUntil(clients.openWindow(url));
}
});
Advanced Usage
Sending to Specific Users
To send notifications to users other than the current authenticated user, create a custom API endpoint:
// /app/api/admin/notify/route.ts
import { createClient } from '@/lib/supabase/server';
import webpush from 'web-push';
export async function POST(request: Request) {
const supabase = await createClient();
// Verify admin privileges
const {
data: { user },
} = await supabase.auth.getUser();
if (!user || !isAdmin(user)) {
return Response.json({ error: 'Unauthorized' }, { status: 403 });
}
const { targetUserId, title, body, url } = await request.json();
// Get subscriptions for target user
const { data: subscriptions } = await supabase
.from('push_subscriptions')
.select('*')
.eq('user_id', targetUserId);
// Send notifications...
// (same logic as /api/notifications/send)
}
Notification Scheduling
For scheduled notifications, use a background job system like:
- Vercel Cron Jobs: For periodic notifications
- Supabase Edge Functions: For event-driven notifications
- Queue systems: Redis Queue, BullMQ for complex scheduling
Example with Vercel Cron:
// /app/api/cron/daily-reminder/route.ts
export async function GET(request: Request) {
// Verify cron secret
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Send daily reminders to all users
const users = await getActiveUsers();
for (const user of users) {
await sendNotification(user.id, {
title: 'Daily Reminder',
body: "Check out what's new today!",
url: '/dashboard',
});
}
return Response.json({ success: true });
}
Configure in vercel.json:
{
"crons": [
{
"path": "/api/cron/daily-reminder",
"schedule": "0 9 * * *"
}
]
}
Analytics & Tracking
Track notification engagement:
// When sending a notification
const notificationId = generateUniqueId();
await fetch('/api/notifications/send', {
method: 'POST',
body: JSON.stringify({
title: 'New Feature',
body: 'Check out our latest update',
url: `/features/new?notificationId=${notificationId}`,
}),
});
// Track in analytics when user clicks
// In your target page:
const params = new URLSearchParams(window.location.search);
if (params.get('notificationId')) {
analytics.track('notification_clicked', {
id: params.get('notificationId'),
});
}
Troubleshooting
Notifications Not Appearing
-
Check browser support: Push is not supported in all browsers (especially older Safari versions)
console.log('Push supported:', 'PushManager' in window); -
Verify VAPID keys: Ensure both public and private keys are set in environment variables
echo $NEXT_PUBLIC_VAPID_PUBLIC_KEY echo $VAPID_PRIVATE_KEY -
Check permissions: User must have granted notification permission
console.log('Permission:', Notification.permission); -
Service Worker registration: Ensure the SW is registered and active
navigator.serviceWorker.getRegistrations().then(console.log); -
Browser console errors: Check for any errors in the browser console or service worker console
Subscription Fails
- VAPID key mismatch: The public key in the browser must match the private key on the server
- Service Worker not active: Wait for the SW to activate before subscribing
- HTTPS required: Push notifications only work on HTTPS (or localhost for testing)
Server-Side Errors
Check your API logs for errors like:
"Missing VAPID keys": SetVAPID_PRIVATE_KEYandVAPID_SUBJECTin environment"No subscriptions found": User hasn't subscribed yet or subscription expired"Push subscription expired": Browser unsubscribed; prompt user to re-subscribe
Testing on iOS
iOS Safari has limited push notification support:
- iOS 16.4+: Requires app to be installed to Home Screen
- Prompt timing: User must interact with the page before permission can be requested
- Background delivery: May be delayed or batched by the OS
Best Practices
User Experience
- ✅ Request permission contextually: Only ask when the user understands the value (e.g., after they subscribe to updates)
- ✅ Provide clear opt-out: Make it easy to unsubscribe in settings
- ✅ Don't spam: Limit notification frequency to avoid user annoyance
- ✅ Make notifications actionable: Include a clear call-to-action and relevant link
- ✅ Respect Do Not Disturb: Consider time zones and quiet hours
Security
- ✅ Never expose private VAPID key: Keep it in server-side environment variables only
- ✅ Validate notification content: Sanitize user input before sending notifications
- ✅ Rate limit API endpoints: Prevent abuse of the send notification endpoint
- ✅ Use HTTPS everywhere: Push notifications require secure contexts
- ✅ Implement user authentication: Only authenticated users should receive notifications
Performance
- ✅ Batch notifications: Group multiple updates into a single notification when possible
- ✅ Use notification tags: Replace old notifications instead of stacking them
- ✅ Handle expired subscriptions: Remove invalid subscriptions from the database
- ✅ Monitor delivery rates: Track success/failure to identify issues early
Compliance
- ✅ GDPR compliance: Treat push subscriptions as personal data; allow users to export/delete
- ✅ Privacy policy: Disclose how notification data is used and stored
- ✅ User consent: Only subscribe users who explicitly opt in
- ✅ Data retention: Set expiration policies for old subscriptions
Further Reading
- Web Push API (MDN)
- Notifications API (MDN)
- Service Worker API (MDN)
- VAPID Specification
- web-push library documentation
Related Documentation
- Getting Started — Initial setup guide
- Architecture — PWA and Service Worker architecture
- Environment Variables — Complete environment configuration reference