Overview
The MLM CMMS frontend implements a Progressive Web App (PWA) with service worker-based caching and Web Push notification support.
Key files:
Service Worker: public/sw.js
Push Service: src/services/pushNotificationsService.ts
Notification Service: src/services/notificationCenterService.ts
Service Worker Features
Caching Strategy
The service worker implements a hybrid caching approach:
Navigation Requests
Static Assets
API Requests
Network-first with timeout fallback
Attempts network fetch with 8-second timeout
Falls back to cached route on failure
Returns app shell (/index.html) if no cache
Final fallback to /offline.html
// public/sw.js:85
async function handleNavigation ( request ) {
try {
const networkResponse = await withTimeout ( fetch ( request ), NAVIGATION_TIMEOUT_MS );
if ( isCacheableResponse ( networkResponse )) {
const cache = await caches . open ( RUNTIME_CACHE );
await cache . put ( request , networkResponse . clone ());
await trimRuntimeCache ();
}
return networkResponse ;
} catch ( _error ) {
const cache = await caches . open ( RUNTIME_CACHE );
const cachedRoute = await cache . match ( request );
if ( cachedRoute ) return cachedRoute ;
const appShell = await caches . match ( '/index.html' );
if ( appShell ) return appShell ;
return caches . match ( '/offline.html' );
}
}
Cache-first with background update
Returns cached version immediately for fast load
Updates cache in background for next visit
Automatically trims runtime cache to 120 entries
// public/sw.js:104
async function handleStaticAsset ( request ) {
const cache = await caches . open ( RUNTIME_CACHE );
const cachedResponse = await cache . match ( request );
const networkPromise = fetch ( request )
. then ( async ( networkResponse ) => {
if ( isCacheableResponse ( networkResponse )) {
await cache . put ( request , networkResponse . clone ());
await trimRuntimeCache ();
}
return networkResponse ;
})
. catch (() => cachedResponse );
return cachedResponse || networkPromise ;
}
Always bypass cache
/api/* and /auth/* routes bypass service worker
Only GET requests are cached
Non-origin requests bypass cache
// public/sw.js:20
function shouldBypass ( request , url ) {
if ( request . method !== 'GET' ) return true ;
if ( url . origin !== self . location . origin ) return true ;
return url . pathname . startsWith ( '/api/' ) || url . pathname . startsWith ( '/auth/' );
}
Precached Resources
These files are cached during service worker installation:
// public/sw.js:9
const PRECACHE_URLS = [
'/' ,
'/index.html' ,
'/offline.html' ,
'/manifest.webmanifest' ,
'/apple-touch-icon.png' ,
'/pwa-192x192.png' ,
'/pwa-512x512.png' ,
'/pwa-maskable-512x512.png' ,
];
Push Notification Handling
Push Event Processing
When a push message arrives, the service worker:
Parse Payload
Extract title, body, URL, and metadata from push data: // public/sw.js:154
self . addEventListener ( 'push' , ( event ) => {
const payload = parsePushPayload ( event );
const title = typeof payload . title === 'string' && payload . title . trim ()
? payload . title
: 'Nueva notificación' ;
const body = typeof payload . body === 'string' && payload . body . trim ()
? payload . body
: 'Tienes una actualización en el sistema.' ;
// ...
});
Show Browser Notification
Display native OS notification with icon and badge: const options = {
body ,
icon: payload . icon || '/pwa-192x192.png' ,
badge: payload . badge || '/pwa-192x192.png' ,
data: {
url: payload . url || '/notificaciones' ,
},
tag: payload . tag ,
renotify: Boolean ( payload . renotify ),
};
await self . registration . showNotification ( title , options );
Broadcast to Open Tabs
Notify all open app windows to refresh notification center: // public/sw.js:35
async function broadcastPushReceived ( payload ) {
const clients = await self . clients . matchAll ({
type: 'window' ,
includeUncontrolled: true ,
});
for ( const client of clients ) {
client . postMessage ({
type: 'notification:push_received' ,
receivedAt: Date . now (),
url: typeof payload . url === 'string' ? payload . url : '/notificaciones' ,
});
}
}
Notification Click Handling
When user clicks a notification:
// public/sw.js:196
self . addEventListener ( 'notificationclick' , ( event ) => {
event . notification . close ();
const targetUrl = new URL (
event . notification . data ?. url || '/notificaciones' ,
self . location . origin
). href ;
event . waitUntil (
self . clients . matchAll ({ type: 'window' , includeUncontrolled: true })
. then (( clients ) => {
// Focus existing tab if URL matches
for ( const client of clients ) {
if ( client . url === targetUrl && 'focus' in client ) {
return client . focus ();
}
}
// Otherwise open new window
if ( self . clients . openWindow ) {
return self . clients . openWindow ( targetUrl );
}
})
);
});
Frontend Integration
Service Worker Registration
The service worker is registered in src/main.tsx with cache-busting:
// src/main.tsx (service worker registration)
if ( 'serviceWorker' in navigator && window . isSecureContext ) {
navigator . serviceWorker
. register ( `/sw.js?v= ${ __APP_VERSION__ } ` , {
updateViaCache: 'none' ,
})
. then (( registration ) => {
console . log ( 'Service Worker registered:' , registration );
})
. catch (( error ) => {
console . error ( 'Service Worker registration failed:' , error );
});
}
The updateViaCache: 'none' option ensures the browser always fetches the latest service worker file.
Push Subscription Management
The pushNotificationsService handles subscription lifecycle:
Subscribe to Push
// src/services/pushNotificationsService.ts:83
export async function subscribeCurrentDeviceToPush (
vapidPublicKey : string
) : Promise < void > {
// 1. Request notification permission
const permission = await Notification . requestPermission ();
if ( permission !== 'granted' ) {
throw new Error ( 'El permiso de notificaciones fue denegado.' );
}
// 2. Ensure service worker is active
const registration = await ensureServiceWorkerRegistration ();
// 3. Subscribe to push manager
let subscription = await registration . pushManager . getSubscription ();
if ( ! subscription ) {
subscription = await registration . pushManager . subscribe ({
userVisibleOnly: true ,
applicationServerKey: urlBase64ToUint8Array ( vapidPublicKey ),
});
}
// 4. Save subscription to database
const json = subscription . toJSON ();
await supabase . from ( 'push_subscriptions' ). upsert ({
user_id: userId ,
endpoint: json . endpoint ,
p256dh: json . keys . p256dh ,
auth: json . keys . auth ,
user_agent: navigator . userAgent ,
platform: navigator . userAgentData ?. platform ?? navigator . platform ,
last_seen_at: new Date (). toISOString (),
}, { onConflict: 'user_id,endpoint' });
}
Unsubscribe from Push
// src/services/pushNotificationsService.ts:144
export async function unsubscribeCurrentDeviceFromPush () : Promise < void > {
const registration = await navigator . serviceWorker . getRegistration ();
const subscription = await registration ?. pushManager . getSubscription ();
// 1. Unsubscribe from browser
if ( subscription ) {
await subscription . unsubscribe ();
}
// 2. Remove from database
await supabase
. from ( 'push_subscriptions' )
. delete ()
. eq ( 'user_id' , userId )
. eq ( 'endpoint' , subscription ?. endpoint );
}
Browser Compatibility Check
// src/services/pushNotificationsService.ts:73
export function isPushSupported () {
return (
typeof window !== 'undefined' &&
window . isSecureContext &&
'Notification' in window &&
'PushManager' in window &&
'serviceWorker' in navigator
);
}
Push notifications require:
HTTPS (or localhost for development)
Modern browser with Push API support
Service Worker support
iOS Safari
Requirements
Testing on iOS
iOS 16.4+ required for Web Push
App must be installed to Home Screen (Add to Home Screen)
User must grant notification permission in Settings after install
Only works in standalone PWA mode, not in-browser
Open Safari and navigate to your site
Tap Share button → “Add to Home Screen”
Launch app from Home Screen icon
Trigger push subscription flow
Go to iOS Settings → Notifications → [Your App]
Enable “Allow Notifications”
Android Chrome
Requirements
Testing on Android
Works in-browser without PWA installation
Notification permission prompt shown on first request
Background sync and notification grouping supported
Open Chrome and navigate to your site
Accept notification permission when prompted
Push notifications work immediately
Optionally install PWA for better UX
Desktop (macOS/Windows/Linux)
Requirements
Testing on Desktop
Chrome, Edge, Firefox supported
Safari 16+ on macOS Ventura+
Notification permission managed by browser/OS
Open browser and navigate to your site
Accept notification permission prompt
Push notifications delivered via OS notification center
Check browser/OS notification settings if not receiving
User Interface Integration
Notification Center
The /notificaciones page provides:
Unread badge : Shows count of unread notifications in navbar bell icon
Filter tabs : All, Unread, Tickets, Admin
Read/unread toggle : Click or swipe to mark
Push subscription toggle : Enable/disable push for current device
Self-test tool : Send test notification to verify setup
Admin test tool : Send notifications to other users (requires permissions)
See src/pages/NotificationCenter.tsx for implementation.
Realtime Updates
The notification center subscribes to database changes:
// src/services/notificationCenterService.ts:565
export function subscribeToMyNotificationDeliveries (
userId : string ,
onChange : () => void
) {
const channelName = `notification-deliveries: ${ userId } : ${ crypto . randomUUID () } ` ;
const channel = supabase
. channel ( channelName )
. on (
'postgres_changes' ,
{
event: '*' ,
schema: 'public' ,
table: 'notification_deliveries' ,
filter: `recipient_user_id=eq. ${ userId } ` ,
},
() => onChange ()
)
. subscribe ();
return () => {
void supabase . removeChannel ( channel );
};
}
Push-Triggered Refresh
When a push notification arrives while the app is open:
// src/main.tsx (message listener)
navigator . serviceWorker . addEventListener ( 'message' , ( event ) => {
if ( event . data ?. type === 'notification:push_received' ) {
// Trigger notification center refresh
window . dispatchEvent ( new CustomEvent ( 'notification:received' , {
detail: { url: event . data . url }
}));
}
});
Testing Push Notifications
Test from UI
Enable Push
Navigate to /notificaciones and click “Activar Push en este dispositivo”
Send Self-Test
Click “Enviar prueba a mí mismo” button (available to all authenticated users)
Verify Receipt
Check browser notification appears
Verify in-app notification list updates
Check outbox status transitions to sent
Test from Database
-- Send test notification to specific user
SELECT send_self_test_notification(
'Prueba manual' ,
'Este es un mensaje de prueba desde SQL' ,
true -- send_push
);
-- Admin test (requires permissions)
SELECT admin_send_test_notification(
'<recipient-user-id>' ::uuid,
'Prueba de admin' ,
'Mensaje enviado por administrador' ,
'/tickets/123' ,
true -- send_push
);
Debug Push Subscription
Inspect subscription details in browser console:
// Check if service worker is registered
navigator . serviceWorker . getRegistration (). then ( reg => {
console . log ( 'SW registration:' , reg );
});
// Check push subscription
navigator . serviceWorker . ready . then ( reg => {
reg . pushManager . getSubscription (). then ( sub => {
console . log ( 'Push subscription:' , sub ?. toJSON ());
});
});
// Check notification permission
console . log ( 'Notification permission:' , Notification . permission );
Troubleshooting
”Permission Denied” Errors
Check notification permission :
Chrome: chrome://settings/content/notifications
Firefox: about:preferences#privacy → Permissions → Notifications
Safari: System Preferences → Notifications
Reset permission (requires browser-specific steps):
Chrome: Site settings → Notifications → Reset
Safari: Remove and re-add to Home Screen
Service Worker Not Updating
Check update strategy :
navigator . serviceWorker . getRegistrations (). then ( regs => {
regs . forEach ( reg => reg . update ());
});
Hard refresh : Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (macOS)
Clear service worker :
Chrome DevTools → Application → Service Workers → Unregister
Push Not Received
Verify subscription exists :
SELECT * FROM push_subscriptions WHERE user_id = '<user-uuid>' ;
Check outbox status :
SELECT * FROM notification_outbox
WHERE delivery_id IN (
SELECT id FROM notification_deliveries
WHERE recipient_user_id = '<user-uuid>'
)
ORDER BY created_at DESC LIMIT 10 ;
Verify Edge Function is deployed (see Edge Functions )
Check browser console for errors during subscription
Reduce Cache Size
Adjust runtime cache limit:
// public/sw.js:6
const MAX_RUNTIME_ENTRIES = 120 ; // Reduce to 60 for slower devices
Disable Push for Low-End Devices
Check device capabilities before offering push:
if ( ! isPushSupported ()) {
// Hide push subscription UI
return ;
}
// Optional: Check connection speed
if ( navigator . connection ?. effectiveType === '2g' ) {
// Warn user about data usage
}
Optimize Notification Payload
Keep push payload minimal (4KB limit):
// Only include essential fields
const payload = {
title: notification . title ,
body: notification . message . slice ( 0 , 200 ), // Truncate long messages
url: notification . url ,
tag: notification . deliveryId , // For deduplication
};
Security Considerations
Never expose VAPID_PRIVATE_KEY in client code
Only store VAPID_PUBLIC_KEY in frontend .env
Validate notification URLs to prevent open redirects
Use HTTPS in production (required for Push API)
URL Validation
The system enforces internal URLs:
// src/services/notificationCenterService.ts:145
function resolveNotificationUrl (
payload : Record < string , unknown >,
entityType : string ,
entityId : string
) {
const payloadUrl = payload . url ;
// Only allow internal paths starting with '/'
if ( typeof payloadUrl === 'string' && payloadUrl . startsWith ( '/' )) {
return payloadUrl ;
}
// Default safe fallback
if ( entityType === 'ticket' ) {
return `/tickets/ ${ entityId } ` ;
}
return '/inicio' ;
}
Next Steps
Notification Setup Return to setup guide for database configuration
Edge Functions Review Edge Function deployment and monitoring