Skip to content

Instantly share code, notes, and snippets.

@agrittiwari
Created May 5, 2025 09:15
Show Gist options
  • Select an option

  • Save agrittiwari/bad34f6cbf2bb65b2fd99b05eb2a3d93 to your computer and use it in GitHub Desktop.

Select an option

Save agrittiwari/bad34f6cbf2bb65b2fd99b05eb2a3d93 to your computer and use it in GitHub Desktop.
Implementing Push Notifications on web using service worker
//backend code
import webPush from "web-push";
import { Prisma } from "@prisma/client";
export type PushNotificationData = {
to: string | string[];
webPushSub: Prisma.JsonValue | Prisma.JsonArray;
title: string;
body: string;
data: {
screen: string;
params: Record<string, unknown>;
};
};
// VAPID keys should be stored in environment variables
const apiKeys = {
publicKey: process.env.WEB_PUSH_PUBLIC_KEY,
privateKey: process.env.WEB_PUSH_PRIVATE_KEY,
};
// Set VAPID details (replace mailto with your own admin email if needed)
webPush.setVapidDetails(
"mailto:admin@example.com",
apiKeys.publicKey!,
apiKeys.privateKey!
);
export const sendPushNotification = async (
data: PushNotificationData
): Promise<{ status: number; message: string }> => {
try {
// Append default sound setting or extend payload here if needed
const updatedData = { ...data, sound: "default" };
// Utility function to send a single web push notification
const sendWebPushNotification = async (subscription: any) => {
try {
await webPush.sendNotification(subscription, JSON.stringify(updatedData));
} catch (err) {
console.error("Failed to send notification:", err);
// Optionally integrate with an error tracking service here
}
};
// Support both single and multiple subscriptions
if (Array.isArray(data.webPushSub)) {
await Promise.all(data.webPushSub.map(sendWebPushNotification));
} else {
await sendWebPushNotification(data.webPushSub);
}
return {
status: 200,
message: "Notifications sent successfully",
};
} catch (err) {
console.error("Error in sendPushNotification:", err);
return {
status: 500,
message: "Failed to send push notifications",
};
}
};
// Load Workbox from CDN
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js');
// Precache assets if Workbox is available
if (workbox) {
console.log('Workbox is loaded');
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
} else {
console.log('Workbox failed to load');
}
const graphqlEndpoint = 'https://your-api-endpoint.com/graphql'; // Replace with your GraphQL endpoint
// Handle fetch to inject auth token
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname === '/graphql') {
event.respondWith(handleRequest(event.request));
}
});
// Activate event: subscribe to push notifications and send subscription to backend
self.addEventListener('activate', async () => {
try {
const subscription = await self.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY'),
});
const query = `mutation UpsertUser($webPushSub: JSON!) {
updateUser(input: { webPushSubscription: $webPushSub }) {
id
webPushSubscription
}
}`;
await savePushNotificationSubscriptionMutation(query, subscription, 'UpsertUser');
} catch (error) {
console.error('Failed to subscribe to push notifications:', error);
}
});
// Display notification when a push event is received
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
const title = data.title || 'Hello!';
const options = {
body: data.body || 'Default notification body',
icon: data.icon || '/assets/icon.png',
};
event.waitUntil(self.registration.showNotification(title, options));
});
// Handle GraphQL request with injected token
async function handleRequest(request) {
const modifiedRequest = new Request(request, {
headers: new Headers(request.headers),
});
const accessToken = await getAccessTokenFromIndexedDB();
if (accessToken) {
modifiedRequest.headers.set('Authorization', `Bearer ${accessToken}`);
}
return fetch(modifiedRequest);
}
// Retrieve token from IndexedDB
function getAccessTokenFromIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('auth-db');
request.onerror = () => {
console.error('Could not open IndexedDB');
reject(null);
};
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['tokens'], 'readonly');
const store = transaction.objectStore('tokens');
const getRequest = store.get('accessToken');
getRequest.onsuccess = () => {
resolve(getRequest.result ? getRequest.result.accessToken : null);
};
getRequest.onerror = () => {
console.error('Could not retrieve access token');
reject(null);
};
};
});
}
// Save subscription to backend
async function savePushNotificationSubscriptionMutation(query, variables, operationName) {
try {
const accessToken = await getAccessTokenFromIndexedDB();
const response = await fetch(graphqlEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
query,
variables: {
webPushSub: variables,
},
operationName,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('GraphQL response:', data);
} catch (error) {
console.error('GraphQL error:', error);
}
}
// VAPID key converter
const urlBase64ToUint8Array = (base64String) => {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
const rawData = atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
module.exports = {
globDirectory: 'public/',
globPatterns: ['**/*.{js,css,html,png,svg}'],
swSrc: 'public/service-worker.js',
swDest: 'public/sw.js',
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment