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

  1. User grants permission: Browser prompts user to allow notifications
  2. Browser subscribes: Creates a unique push subscription endpoint
  3. Server stores subscription: Saved to Supabase push_subscriptions table
  4. Server sends notification: Uses web-push library to deliver payload
  5. Service Worker receives: Displays notification with custom actions
  6. 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.):

  1. Add these environment variables in your hosting provider's dashboard
  2. Ensure VAPID_PRIVATE_KEY is marked as secret/sensitive
  3. 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_subscriptions table 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

  1. Start your development server:

    pnpm dev
    
  2. Navigate to the demo page:

    http://localhost:3000/en/demo/notifications
    
  3. Click "Enable Notifications" and grant permission when prompted

  4. 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:

  1. Verify the user is authenticated
  2. Look up all push subscriptions for that user
  3. Send the notification to each subscription
  4. 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

  1. Check browser support: Push is not supported in all browsers (especially older Safari versions)

    console.log('Push supported:', 'PushManager' in window);
    
  2. Verify VAPID keys: Ensure both public and private keys are set in environment variables

    echo $NEXT_PUBLIC_VAPID_PUBLIC_KEY
    echo $VAPID_PRIVATE_KEY
    
  3. Check permissions: User must have granted notification permission

    console.log('Permission:', Notification.permission);
    
  4. Service Worker registration: Ensure the SW is registered and active

    navigator.serviceWorker.getRegistrations().then(console.log);
    
  5. 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": Set VAPID_PRIVATE_KEY and VAPID_SUBJECT in 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

Related Documentation