Push Notification Issues
What This Means
Web push notifications allow you to re-engage users even when they're not on your site. Common issues:
- Permission prompts not appearing
- Notifications not being delivered
- Notifications not displaying correctly
- Users not receiving notifications after granting permission
Prerequisites
Push notifications require:
- HTTPS (mandatory)
- Service worker registered
- User permission granted
- Push subscription created
- Backend push service configured
How to Diagnose
1. Check Permission Status
// Check current permission
console.log('Notification permission:', Notification.permission);
// 'default' - not asked yet
// 'granted' - permission given
// 'denied' - permission blocked
2. Service Worker Status
DevTools > Application > Service Workers:
- Verify SW is active
- Check for push event handler
3. Push Subscription
// Check if push is subscribed
navigator.serviceWorker.ready.then(registration => {
registration.pushManager.getSubscription().then(subscription => {
console.log('Push subscription:', subscription);
});
});
4. Browser Support
// Check push support
const supported = 'PushManager' in window;
console.log('Push supported:', supported);
General Fixes
Request Permission Properly
// Request permission with user interaction
async function requestNotificationPermission() {
// Check if already granted/denied
if (Notification.permission === 'granted') {
return true;
}
if (Notification.permission === 'denied') {
console.log('Notifications blocked by user');
return false;
}
// Request permission
const permission = await Notification.requestPermission();
return permission === 'granted';
}
// Trigger on user action (required in modern browsers)
document.getElementById('enable-notifications').addEventListener('click', async () => {
const granted = await requestNotificationPermission();
if (granted) {
await subscribeToPush();
showSuccessMessage();
}
});
Subscribe to Push
// Subscribe user to push notifications
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
// Get VAPID public key from your server
const vapidPublicKey = 'YOUR_VAPID_PUBLIC_KEY';
const convertedKey = urlBase64ToUint8Array(vapidPublicKey);
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, // Required: must show notification
applicationServerKey: convertedKey
});
// Send subscription to your server
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
return subscription;
}
// Helper function
function 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;
}
Service Worker Push Handler
// sw.js - Handle push events
self.addEventListener('push', (event) => {
console.log('Push received:', event);
let data = { title: 'Notification', body: 'You have a new message' };
if (event.data) {
try {
data = event.data.json();
} catch (e) {
data.body = event.data.text();
}
}
const options = {
body: data.body,
icon: '/icons/notification-icon.png',
badge: '/icons/badge-icon.png',
image: data.image,
vibrate: [100, 50, 100],
data: {
url: data.url || '/',
dateOfArrival: Date.now()
},
actions: [
{ action: 'open', title: 'Open' },
{ action: 'dismiss', title: 'Dismiss' }
]
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// Handle notification click
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'dismiss') {
return;
}
const url = event.notification.data?.url || '/';
event.waitUntil(
clients.matchAll({ type: 'window' }).then(clientList => {
// Focus existing tab if open
for (const client of clientList) {
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
// Open new tab
return clients.openWindow(url);
})
);
});
Backend Push Service (Node.js)
// server.js
const webpush = require('web-push');
// Generate VAPID keys (run once)
// const vapidKeys = webpush.generateVAPIDKeys();
webpush.setVapidDetails(
'mailto:admin@example.com',
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
);
async function sendPushNotification(subscription, payload) {
try {
await webpush.sendNotification(
subscription,
JSON.stringify(payload)
);
console.log('Push sent successfully');
} catch (error) {
if (error.statusCode === 410) {
// Subscription expired, remove from database
await removeSubscription(subscription.endpoint);
}
throw error;
}
}
// Example: Send notification
app.post('/api/push/send', async (req, res) => {
const { userId, title, body, url } = req.body;
const subscriptions = await getSubscriptionsForUser(userId);
await Promise.all(
subscriptions.map(sub =>
sendPushNotification(sub, { title, body, url })
)
);
res.json({ success: true });
});
Permission UI Best Practices
// Don't immediately ask for permission
// Show value proposition first
function showNotificationPromo() {
const promo = document.getElementById('notification-promo');
promo.innerHTML = `
<h3>Stay Updated</h3>
<p>Get notified about price drops and new features</p>
<button id="enable-push">Enable Notifications</button>
<button id="maybe-later">Maybe Later</button>
`;
promo.style.display = 'block';
document.getElementById('enable-push').onclick = async () => {
promo.style.display = 'none';
const granted = await requestNotificationPermission();
if (granted) {
await subscribeToPush();
}
};
document.getElementById('maybe-later').onclick = () => {
promo.style.display = 'none';
localStorage.setItem('push-promo-dismissed', Date.now());
};
}
Verification
- Permission granted:
Notification.permission === 'granted' - Subscription exists in PushManager
- Test notification from backend arrives
- Clicking notification opens correct URL
Common Mistakes
| Mistake | Fix |
|---|---|
| Requesting permission on page load | Wait for user interaction |
Missing userVisibleOnly: true |
Required in subscription options |
| Not handling expired subscriptions | Check for 410 status |
| No notification click handler | Add notificationclick listener |