Skip to main content

Command Palette

Search for a command to run...

Push Notifications: Engage Users on Web Mobile

Learn: Push Notifications: Engage Users on Web Mobile

Updated
6 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Push Notifications: Engage Users on Web & Mobile

Problem

Users leave web applications and forget to return. Without a way to re-engage them, businesses lose valuable interactions, conversions, and user retention. Traditional email is slow and intrusive. Real-time notifications are needed to:

  • Remind users of important updates
  • Drive immediate action on time-sensitive events
  • Increase app engagement and retention
  • Deliver personalized, timely messages
  • Work across web and mobile platforms seamlessly

Solution

Service Workers + Push Notifications API provide a native, browser-based solution for sending real-time notifications even when users aren't actively using your app. This combination enables:

  1. Background Processing: Service workers run independently of the main thread
  2. Push Events: Receive notifications from a server via Push API
  3. User Engagement: Display rich, interactive notifications
  4. Cross-Platform: Works on web, PWAs, and mobile browsers
  5. Persistent: Notifications survive app closures and browser restarts

Architecture Flow

Server → Push Service (FCM/APNs) → Browser → Service Worker → Notification

Code Implementation

1. Register Service Worker

// main.js - Register and request notification permission
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('Service Worker registered:', registration);
      requestNotificationPermission(registration);
    })
    .catch(error => console.error('SW registration failed:', error));
}

function requestNotificationPermission(registration) {
  if ('Notification' in window && Notification.permission === 'default') {
    Notification.requestPermission().then(permission => {
      if (permission === 'granted') {
        subscribeUserToPush(registration);
      }
    });
  }
}

2. Subscribe to Push Notifications

// Subscribe user and send endpoint to server
async function subscribeUserToPush(registration) {
  try {
    const subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(
        'YOUR_PUBLIC_VAPID_KEY'
      )
    });

    // Send subscription to backend
    await fetch('/api/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(subscription)
    });

    console.log('User subscribed to push notifications');
  } catch (error) {
    console.error('Push subscription failed:', error);
  }
}

// Convert VAPID key from base64
function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/');
  const rawData = window.atob(base64);
  return new Uint8Array([...rawData].map(char => char.charCodeAt(0)));
}

3. Service Worker - Handle Push Events

// sw.js - Service Worker
self.addEventListener('push', event => {
  console.log('Push notification received:', event);

  let notificationData = {
    title: 'New Notification',
    body: 'You have a new message',
    icon: '/icon-192x192.png',
    badge: '/badge-72x72.png',
    tag: 'notification-tag',
    requireInteraction: false
  };

  // Parse incoming push data
  if (event.data) {
    try {
      notificationData = event.data.json();
    } catch (e) {
      notificationData.body = event.data.text();
    }
  }

  event.waitUntil(
    self.registration.showNotification(
      notificationData.title,
      {
        body: notificationData.body,
        icon: notificationData.icon,
        badge: notificationData.badge,
        tag: notificationData.tag,
        requireInteraction: notificationData.requireInteraction,
        data: notificationData.data || {},
        actions: [
          { action: 'open', title: 'Open' },
          { action: 'close', title: 'Close' }
        ]
      }
    )
  );
});

// Handle notification clicks
self.addEventListener('notificationclick', event => {
  event.notification.close();

  if (event.action === 'close') {
    return;
  }

  const urlToOpen = event.notification.data.url || '/';

  event.waitUntil(
    clients.matchAll({ type: 'window', includeUncontrolled: true })
      .then(clientList => {
        // Check if app is already open
        for (let i = 0; i < clientList.length; i++) {
          const client = clientList[i];
          if (client.url === urlToOpen && 'focus' in client) {
            return client.focus();
          }
        }
        // Open new window if not already open
        if (clients.openWindow) {
          return clients.openWindow(urlToOpen);
        }
      })
  );
});

// Handle notification close
self.addEventListener('notificationclose', event => {
  console.log('Notification closed:', event.notification.tag);
});

4. Backend - Send Push Notifications (Node.js)

// server.js - Express backend
const express = require('express');
const webpush = require('web-push');

const app = express();
app.use(express.json());

// Set VAPID details
webpush.setVapidDetails(
  'mailto:your-email@example.com',
  process.env.PUBLIC_VAPID_KEY,
  process.env.PRIVATE_VAPID_KEY
);

// Store subscriptions (use database in production)
const subscriptions = [];

// Subscribe endpoint
app.post('/api/subscribe', (req, res) => {
  const subscription = req.body;
  subscriptions.push(subscription);
  res.status(201).json({ message: 'Subscribed successfully' });
});

// Send notification to all users
app.post('/api/notify', async (req, res) => {
  const { title, body, url } = req.body;

  const notificationPayload = {
    title,
    body,
    icon: '/icon-192x192.png',
    data: { url: url || '/' }
  };

  try {
    const promises = subscriptions.map(subscription =>
      webpush.sendNotification(
        subscription,
        JSON.stringify(notificationPayload)
      ).catch(error => {
        if (error.statusCode === 410) {
          // Remove invalid subscription
          subscriptions.splice(subscriptions.indexOf(subscription), 1);
        }
      })
    );

    await Promise.all(promises);
    res.status(200).json({ message: 'Notifications sent' });
  } catch (error) {
    console.error('Error sending notifications:', error);
    res.status(500).json({ error: 'Failed to send notifications' });
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));

5. Generate VAPID Keys

# Install web-push CLI
npm install -g web-push

# Generate VAPID keys
web-push generate-vapid-keys

# Output:
# Public Key: BEn...
# Private Key: xyz...

Tips & Best Practices

1. Permission Strategy

  • Request permission at the right moment (after user engagement)
  • Explain why notifications are valuable
  • Respect user preferences and provide opt-out options
// Request after user interaction
button.addEventListener('click', () => {
  Notification.requestPermission();
});

2. Notification Content

  • Keep titles concise (< 50 characters)
  • Use clear, actionable body text
  • Include relevant data for deep linking
  • Add action buttons for common interactions
{
  title: '🎉 Order Confirmed',
  body: 'Your order #12345 is being prepared',
  tag: 'order-12345', // Prevents duplicates
  data: { orderId: '12345', url: '/orders/12345' }
}

3. Frequency & Timing

  • Avoid notification fatigue (max 2-3 per day)
  • Send at optimal times based on user timezone
  • Batch non-urgent notifications
  • Respect quiet hours (9 PM - 8 AM)

4. Error Handling

  • Handle 410 (Gone) responses - subscription expired
  • Implement retry logic with exponential backoff
  • Monitor delivery failures
  • Clean up invalid subscriptions
webpush.sendNotification(subscription, payload)
  .catch(error => {
    if (error.statusCode === 410) {
      // Remove subscription
    } else if (error.statusCode >= 500) {
      // Retry later
    }
  });

5. Security

  • Use HTTPS only (required for Service Workers)
  • Validate VAPID keys in environment variables
  • Sanitize notification data to prevent XSS
  • Implement rate limiting on notification endpoints

6. Testing

  • Test on multiple browsers (Chrome, Firefox, Safari)
  • Test on mobile devices and PWAs
  • Simulate offline scenarios
  • Monitor notification delivery rates

7. Analytics

  • Track notification delivery, clicks, and dismissals
  • Measure engagement impact
  • A/B test notification content
  • Monitor unsubscribe rates
// Track in Service Worker
self.addEventListener('notificationclick', event => {
  fetch('/api/analytics', {
    method: 'POST',
    body: JSON.stringify({
      event: 'notification_click',
      tag: event.notification.tag,
      timestamp: new Date()
    })
  });
});

8. Browser Support

  • Chrome/Edge: Full support
  • Firefox: Full support
  • Safari: Limited (iOS 16.4+)
  • IE: Not supported

9. Rich Notifications

  • Add images, badges, and custom styling
  • Use action buttons for quick responses
  • Include notification groups/tags
  • Support dark mode
{
  title: 'New Message',
  body: 'From John Doe',
  image: '/message-image.jpg',
  badge: '/badge.png',
  tag: 'messages',
  requireInteraction: true
}

10. Unsubscribe Handling

  • Provide easy unsubscribe mechanism
  • Handle subscription expiration gracefully
  • Allow notification preference management
  • Respect user choices immediately
// Unsubscribe user
async function unsubscribeUser() {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.getSubscription();
  if (subscription) {
    await subscription.unsubscribe();
  }
}

Conclusion

Push notifications via Service Workers provide a powerful, native way to re-engage users and drive meaningful interactions. By combining proper permission handling, strategic timing, and rich content, you can significantly improve user retention and satisfaction while respecting user preferences and privacy.