CloudSignal PWA SDK - Progressive Web App features with push notifications, JWT/HMAC authentication, installation management, device tracking, offline queue, wake lock, and notification analytics
npm install @cloudsignal/pwa-sdkProgressive Web App SDK for CloudSignal platform with push notifications, installation management, comprehensive device tracking, offline resilience, and notification analytics.
registerInstallation() method for manual installation trackingisInstallationRegistered() / getInstallationId() helper methodsinstall:registered event for installation trackingonTokenExpired callback``bash`
npm install @cloudsignal/pwa-sdk
`html
`
Set up your environment variables before using the SDK:
`bash`.env.local (Next.js) or .env
NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID=your-org-uuid
NEXT_PUBLIC_CLOUDSIGNAL_SERVICE_ID=your-service-uuid
NEXT_PUBLIC_CLOUDSIGNAL_ORG_SECRET=your-secret-key # For HMAC mode only
Where to get these values:
1. Organization ID - From CloudSignal dashboard → Organization Settings
2. Service ID - From CloudSignal dashboard → PWA Services → Your Service
3. Organization Secret - From CloudSignal dashboard → API Keys (for HMAC mode)
`javascript
import { CloudSignalPWA } from '@cloudsignal/pwa-sdk'
const pwa = new CloudSignalPWA({
organizationId: 'your-org-uuid',
organizationSecret: 'your-secret-key',
serviceId: 'your-service-uuid',
debug: true
})
// Initialize (downloads config, registers service worker)
await pwa.initialize()
// Get device information
const deviceInfo = pwa.getDeviceInfo()
console.log('Device:', deviceInfo.deviceModel, deviceInfo.browser)
// Check installation state - returns InstallationState object
const installState = pwa.getInstallationState()
if (installState.canBeInstalled && !installState.isInstalled) {
await pwa.showInstallPrompt()
}
// Register for push notifications
const registration = await pwa.registerForPush({
userEmail: 'user@example.com'
})
// Access registration ID (note: registrationId, not id)
if (registration) {
console.log('Registered with ID:', registration.registrationId)
}
`
`html`
For authenticated user registrations, use JWT instead of HMAC:
`javascript
import { CloudSignalPWA } from '@cloudsignal/pwa-sdk'
import { supabase } from './supabase' // Your Supabase client
const pwa = new CloudSignalPWA({
organizationId: 'your-org-uuid',
userToken: (await supabase.auth.getSession()).data.session?.access_token,
serviceId: 'your-service-uuid',
onTokenExpired: async () => {
// Refresh token when 401 received
const { data } = await supabase.auth.refreshSession()
return data.session?.access_token || ''
}
})
await pwa.initialize()
// Registration will be linked to the authenticated user
const registration = await pwa.registerForPush()
`
Upgrading from Anonymous to Authenticated:
`javascript
// Start with HMAC (anonymous)
const pwa = new CloudSignalPWA({
organizationId: 'your-org-uuid',
organizationSecret: 'your-secret-key',
serviceId: 'your-service-uuid'
})
await pwa.initialize()
await pwa.registerForPush() // Anonymous registration
// Later, when user logs in:
const userToken = await getUserJWT()
pwa.setUserToken(userToken)
await pwa.registerForPush() // Re-register with user identity
`
`typescript`
new CloudSignalPWA(config: PWAConfig)
Config Options:
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| organizationId | string | Yes | CloudSignal organization UUID |organizationSecret
| | string | No* | Organization secret key (HMAC mode) |userToken
| | string | No* | JWT token from identity provider |onTokenExpired
| | function | No | Callback to refresh JWT on 401 |serviceId
| | string | Yes | PWA service UUID |serviceUrl
| | string | No | Service URL (default: https://pwa.cloudsignal.app) |debug
| | boolean | No | Enable debug logging |serviceWorker
| | object | No | Service worker config |heartbeat
| | object | No | Heartbeat config |
*Either organizationSecret or userToken must be provided
`typescript
// Initialize the PWA client
await pwa.initialize(): Promise
// Download service configuration
await pwa.downloadConfig(): Promise
`
`typescript
// Show install prompt (Chrome/Edge)
await pwa.showInstallPrompt(): Promise
// Check if PWA can be installed
pwa.canInstall(): boolean
// Check if PWA is already installed
pwa.isInstalled(): boolean
// Get installation state (full details)
pwa.getInstallationState(): InstallationState
// Returns: {
// isInstalled: boolean,
// canBeInstalled: boolean,
// needsManualInstall: boolean,
// showManualInstructions: boolean,
// installSteps: string[],
// displayMode: 'browser' | 'standalone' | 'minimal-ui' | 'fullscreen'
// }
// Get install steps for current platform
pwa.getInstallSteps(): string[]
// Register installation with backend (v1.2.3)
await pwa.registerInstallation(): Promise<{ registrationId: string } | null>
// Check if installation is registered (v1.2.3)
pwa.isInstallationRegistered(): boolean
// Get installation registration ID (v1.2.3)
pwa.getInstallationId(): string | null
`
`typescript
// Register for push notifications
await pwa.registerForPush(options?: RegisterOptions): Promise
// Unregister from push notifications
await pwa.unregisterFromPush(): Promise
// Update notification preferences
await pwa.updatePreferences(prefs: NotificationPreferences): Promise
// Check registration status
await pwa.checkRegistrationStatus(): Promise
// Request notification permission
await pwa.requestPermission(): Promise
// Check if registered
pwa.isRegistered(): boolean
// Get registration ID
pwa.getRegistrationId(): string | null
`
`typescript
// Get comprehensive device info (35+ fields)
pwa.getDeviceInfo(): DeviceInfo
// Get PWA capabilities
pwa.getCapabilities(): PWACapabilities
`
`typescript
// Start heartbeat for online status tracking
pwa.startHeartbeat(): void
// Stop heartbeat
pwa.stopHeartbeat(): void
// Get current interval (may vary with network conditions)
pwa.getHeartbeatInterval(): number
// Get network connection info
pwa.getNetworkInfo(): NetworkConnectionInfo
// Get battery info (if available)
await pwa.getBatteryInfo(): Promise
`
`typescript
// Get current authentication mode
pwa.getAuthMode(): 'hmac' | 'jwt'
// Set/upgrade JWT token (for authenticated users)
pwa.setUserToken(token: string): void
// After calling setUserToken, call registerForPush() again to link registration to user
`
`typescript
// Request screen wake lock (prevents screen from sleeping)
await pwa.requestWakeLock(): Promise
// Release wake lock
pwa.releaseWakeLock(): void
// Get current state
pwa.getWakeLockState(): WakeLockState
// Returns: { isSupported: boolean, isActive: boolean }
`
`typescript
// Queue a request for later (when offline)
// Signature: queueRequest(url, method, options)
await pwa.queueOfflineRequest(
url: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
options?: {
headers?: Record
body?: string,
requestType?: 'registration' | 'heartbeat' | 'analytics' | 'preferences' | 'unregister' | 'custom',
priority?: number,
maxRetries?: number,
metadata?: Record
}
): Promise
// Process queued requests - returns array of results
await pwa.processOfflineQueue(): Promise
// QueueProcessResult: { id: number, success: boolean, statusCode?: number, error?: string, shouldRetry: boolean }
// Get queue statistics
await pwa.getOfflineQueueStats(): Promise
// OfflineQueueStats: { totalQueued: number, byType: Record
// Clear all queued requests
await pwa.clearOfflineQueue(): Promise
`
`typescript
// Show iOS install guidance banner
pwa.showIOSInstallBanner(): void
// Hide banner
pwa.hideIOSInstallBanner(): void
// Check if previously dismissed
pwa.wasIOSBannerDismissed(): boolean
// Reset dismissal state
pwa.resetIOSBannerDismissal(): void
`
`typescript
// Clear app badge
pwa.clearBadge(): void
// Set app badge count
pwa.setBadge(count: number): void
`
`typescript
// Subscribe to events
pwa.on(event: PWAEvent, handler: (data) => void): void
// Unsubscribe from events
pwa.off(event: PWAEvent, handler: (data) => void): void
`
Available Events:
Installation Events:
- install:available - Install prompt is available. Handler: (data: { platforms: string[] }) => voidinstall:accepted
- - User accepted install. Handler: (data: { outcome: 'accepted', platform?: string }) => voidinstall:dismissed
- - User dismissed install. Handler: (data: { outcome: 'dismissed' }) => voidinstall:completed
- - PWA was installedinstall:registered
- - PWA installation registered with backend (v1.2.3). Handler: (data: { registrationId: string }) => voidinstall:error
- - Installation error occurred
Push Notification Events:
- push:registered - Push registration successful. Handler: (data: { registrationId: string, endpoint: string }) => voidpush:unregistered
- - Push unregistration successfulpush:updated
- - Push registration updatedpush:error
- - Push operation failed. Handler: (data: { error: Error }) => voidpush:received
- - Push notification received. Handler: (data: { payload: NotificationPayload, timestamp: number }) => voidpush:clicked
- - Notification clicked. Handler: (data: { action?: string, data?: any, url?: string }) => void
Permission Events:
- permission:granted - Notification permission grantedpermission:denied
- - Notification permission deniedpermission:prompt
- - Permission prompt shown
Service Worker Events:
- sw:registered - Service worker registeredsw:updated
- - Service worker updatedsw:error
- - Service worker errorsw:activated
- - Service worker activated
Config Events:
- config:loaded - Service config downloaded. Handler: (data: { config: PWAServiceConfig }) => voidconfig:error
- - Config download failed
Heartbeat Events:
- heartbeat:started - Heartbeat startedheartbeat:stopped
- - Heartbeat stoppedheartbeat:sent
- - Heartbeat sent successfullyheartbeat:error
- - Heartbeat failedheartbeat:intervalChanged
- - Heartbeat interval adjusted (v1.1.0)heartbeat:pausedForBattery
- - Heartbeat paused due to low battery (v1.1.0)heartbeat:resumedFromBattery
- - Heartbeat resumed (v1.1.0)
Network Events:
- network:online - Network came onlinenetwork:offline
- - Network went offline
Wake Lock Events (v1.1.0):
- wakeLock:acquired - Screen wake lock acquiredwakeLock:released
- - Screen wake lock releasedwakeLock:error
- - Wake lock operation failed
Offline Queue Events (v1.1.0):
- offlineQueue:queued - Request added to offline queueofflineQueue:processed
- - Queued request processedofflineQueue:flushed
- - All queued requests processed
iOS Banner Events (v1.1.0):
- iosBanner:shown - iOS install banner showniosBanner:dismissed
- - iOS install banner dismissediosBanner:installClicked
- - User clicked install on iOS banner
Authentication Events (v1.2.0):
- auth:tokenUpdated - JWT token updated/upgraded
State Events:
- state:changed - SDK state changed
IMPORTANT: The service worker MUST be placed at your app's root (e.g., /service-worker.js or /public/service-worker.js) to ensure correct scope. Service workers can only control pages within their scope.
Copy the service worker to your app's root directory:
`bash`If using npm
cp node_modules/@cloudsignal/pwa-sdk/dist/service-worker.js public/
Or download from CDN:
`bash`
curl -o public/service-worker.js https://cdn.cloudsignal.io/service-worker.v1.0.0.js
If you need a different path or filename:
`javascript`
const pwa = new CloudSignalPWA({
// ...other config
serviceWorker: {
path: '/sw.js', // Custom path
scope: '/', // Must match or be parent of your app routes
autoRegister: true,
updateBehavior: 'auto' // 'prompt' | 'auto' | 'manual'
}
})
IMPORTANT: CloudSignal's service worker is not compatible with Workbox, Serwist, or other PWA service worker libraries. You must use one or the other.
Why? CloudSignal's service worker handles:
- Push notification reception and display
- Dynamic manifest downloading and caching
- Notification click routing
- Badge management
- Offline notification history
These features require full control of the service worker lifecycle.
If you're currently using Workbox/Serwist:
1. Remove the existing service worker library (@serwist/next, next-pwa, workbox-webpack-plugin, etc.)
2. Use CloudSignal's service worker instead
3. If you need precaching, consider using the browser's native Cache API in your application code
If you need features from Workbox (like precaching):
Open an issue on GitHub - we may add these capabilities to the CloudSignal service worker in future versions.
Create a manifest.json in your app's root:
`json`
{
"name": "Your App Name",
"short_name": "App",
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff",
"icons": [
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Add to your HTML:
`html`
The SDK detects 35+ device attributes:
`javascript
const info = pwa.getDeviceInfo()
// Operating System
info.os // 'iOS', 'Android', 'Windows', 'macOS', 'Linux'
info.osVersion // '17.2', '14.0', 'Windows 10/11'
// Device
info.deviceType // 'iPhone', 'iPad', 'Phone', 'Tablet', 'Desktop'
info.deviceModel // 'iPhone 15 Pro', 'Samsung Galaxy S24'
info.isMobile // true/false
info.isTablet // true/false
info.isDesktop // true/false
// Browser
info.browser // 'Chrome', 'Safari', 'Firefox', 'Edge'
info.browserVersion // '120.0.6099.43'
// PWA Capabilities
info.supportLevel // 'full', 'partial', 'basic', 'none'
info.hasPushManager // true/false
info.hasServiceWorker // true/false
info.hasBadgeAPI // true/false
info.notificationPermission // 'granted', 'denied', 'default'
// Network
info.isOnline // true/false
info.connectionType // '4g', '3g', '2g', 'unknown'
`
IMPORTANT: The SDK uses browser APIs and must be loaded client-side only.
`tsx
// components/CloudSignalProvider.tsx
'use client'
import { useEffect, useState, createContext, useContext, ReactNode } from 'react'
import type { CloudSignalPWA as CloudSignalPWAType } from '@cloudsignal/pwa-sdk'
type PWAContextType = {
pwa: CloudSignalPWAType | null
isInitialized: boolean
canInstall: boolean
isRegistered: boolean
}
const PWAContext = createContext
pwa: null,
isInitialized: false,
canInstall: false,
isRegistered: false
})
export function CloudSignalProvider({ children }: { children: ReactNode }) {
const [pwa, setPwa] = useState
const [isInitialized, setIsInitialized] = useState(false)
const [canInstall, setCanInstall] = useState(false)
const [isRegistered, setIsRegistered] = useState(false)
useEffect(() => {
// Dynamic import - required for Next.js App Router
const initPWA = async () => {
try {
const { CloudSignalPWA } = await import('@cloudsignal/pwa-sdk')
const instance = new CloudSignalPWA({
organizationId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID!,
organizationSecret: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_SECRET!,
serviceId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_SERVICE_ID!,
debug: process.env.NODE_ENV === 'development'
})
await instance.initialize()
setPwa(instance)
setIsInitialized(true)
setCanInstall(instance.canInstall())
setIsRegistered(instance.isRegistered())
// Set up event listeners
instance.on('install:available', () => setCanInstall(true))
instance.on('install:completed', () => setCanInstall(false))
instance.on('push:registered', () => setIsRegistered(true))
instance.on('push:unregistered', () => setIsRegistered(false))
} catch (error) {
console.error('Failed to initialize CloudSignal PWA:', error)
}
}
initPWA()
}, [])
return (
{children}
)
}
export const usePWA = () => useContext(PWAContext)
`
`tsx
// app/layout.tsx
import { CloudSignalProvider } from '@/components/CloudSignalProvider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
`tsx
// components/InstallButton.tsx
'use client'import { usePWA } from './CloudSignalProvider'
export function InstallButton() {
const { pwa, canInstall } = usePWA()
if (!canInstall) return null
const handleInstall = async () => {
const result = await pwa?.showInstallPrompt()
if (result?.accepted) {
console.log('App installed!')
}
}
return
}
`$3
`tsx
// components/CloudSignalSupabaseProvider.tsx
'use client'import { useEffect, useState, createContext, useContext, ReactNode } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import type { CloudSignalPWA as CloudSignalPWAType } from '@cloudsignal/pwa-sdk'
const supabase = createClientComponentClient()
type PWAContextType = {
pwa: CloudSignalPWAType | null
isInitialized: boolean
}
const PWAContext = createContext({ pwa: null, isInitialized: false })
export function CloudSignalSupabaseProvider({ children }: { children: ReactNode }) {
const [pwa, setPwa] = useState(null)
const [isInitialized, setIsInitialized] = useState(false)
useEffect(() => {
let instance: CloudSignalPWAType | null = null
const initPWA = async () => {
const { CloudSignalPWA } = await import('@cloudsignal/pwa-sdk')
const { data: { session } } = await supabase.auth.getSession()
instance = new CloudSignalPWA({
organizationId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID!,
serviceId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_SERVICE_ID!,
// Use JWT if user is logged in, fall back to HMAC
userToken: session?.access_token,
organizationSecret: !session ? process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_SECRET : undefined,
onTokenExpired: async () => {
const { data } = await supabase.auth.refreshSession()
return data.session?.access_token || ''
}
})
await instance.initialize()
setPwa(instance)
setIsInitialized(true)
}
initPWA()
// Listen for auth changes to update token
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
if (instance && session?.access_token) {
instance.setUserToken(session.access_token)
// Re-register to link to user
await instance.registerForPush()
}
}
)
return () => {
subscription.unsubscribe()
}
}, [])
return (
{children}
)
}
export const usePWA = () => useContext(PWAContext)
`$3
`jsx
import { useEffect, useState } from 'react'
import { CloudSignalPWA } from '@cloudsignal/pwa-sdk'const pwa = new CloudSignalPWA({
organizationId: process.env.REACT_APP_ORG_ID,
organizationSecret: process.env.REACT_APP_ORG_SECRET,
serviceId: process.env.REACT_APP_SERVICE_ID
})
function App() {
const [canInstall, setCanInstall] = useState(false)
const [isRegistered, setIsRegistered] = useState(false)
useEffect(() => {
pwa.initialize().then(() => {
setCanInstall(pwa.canInstall())
setIsRegistered(pwa.isRegistered())
})
pwa.on('install:available', () => setCanInstall(true))
pwa.on('push:registered', () => setIsRegistered(true))
}, [])
const handleInstall = async () => {
const result = await pwa.showInstallPrompt()
if (result.accepted) {
setCanInstall(false)
}
}
const handleEnableNotifications = async () => {
const registration = await pwa.registerForPush({
userEmail: 'user@example.com'
})
if (registration) {
setIsRegistered(true)
}
}
return (
{canInstall && (
)}
{!isRegistered && (
)}
)
}
`Browser Support
| Browser | PWA Install | Push Notifications |
|---------|-------------|-------------------|
| Chrome | ✅ | ✅ |
| Edge | ✅ | ✅ |
| Firefox | ❌ | ✅ |
| Safari (macOS) | ❌ | ✅ (macOS 13+) |
| Safari (iOS) | Manual | ✅ (iOS 16.4+) |
| Samsung Internet | ✅ | ✅ |
TypeScript
Full TypeScript support with exported types:
`typescript
import {
CloudSignalPWA,
PWAConfig,
DeviceInfo,
InstallationState,
Registration,
PWAEvent,
// v1.1.0 types
WakeLockState,
WakeLockConfig,
OfflineQueueConfig,
IOSInstallBannerConfig,
NetworkConnectionInfo,
BatteryInfo,
QueueProcessResult,
OfflineQueueStats,
} from '@cloudsignal/pwa-sdk'
`v1.1.0 Configuration
`javascript
const pwa = new CloudSignalPWA({
organizationId: 'your-org-uuid',
organizationSecret: 'your-secret-key',
serviceId: 'your-service-uuid',
// Wake Lock (prevents screen sleep)
wakeLock: {
enabled: true,
autoRequest: false, // Request manually when needed
reacquireOnVisibility: true,
},
// Offline Queue (stores failed requests)
offlineQueue: {
enabled: true,
maxQueueSize: 100,
maxAgeTTLHours: 24,
autoProcessOnOnline: true,
},
// iOS Install Banner
iosInstallBanner: {
enabled: true,
appName: 'My PWA',
showOnFirstVisit: true,
showDelay: 3000,
dismissRememberDays: 7,
},
// Notification Analytics
notificationAnalytics: {
enabled: true,
endpoint: 'https://api.example.com/analytics', // Optional custom endpoint
},
})
`Troubleshooting
$3
Problem: Service worker not registering
`
DOMException: Failed to register a ServiceWorker
`Solutions:
1. Ensure service worker is at root path (
/service-worker.js)
2. Check HTTPS is enabled (required except on localhost)
3. Verify service worker path in config matches actual file location
4. Check browser console for detailed errorProblem: Push notifications not received after service worker update
Solution: The SDK handles this automatically with
updateBehavior: 'auto'. If using manual mode, call pwa.getServiceWorkerManager().update() after deploy.$3
Problem: Permission prompt never appears
Causes:
1. User previously denied permission (check
pwa.getDeviceInfo().notificationPermission)
2. Site not served over HTTPS
3. Browser settings blocking notifications globallySolution for denied permission:
`javascript
const deviceInfo = pwa.getDeviceInfo()
if (deviceInfo.notificationPermission === 'denied') {
// Guide user to browser settings
alert('Please enable notifications in your browser settings')
}
`$3
Problem: Install prompt not showing on iOS Safari
Explanation: iOS Safari doesn't support the Web App Install Banner API. Use the iOS Install Banner feature:
`javascript
const pwa = new CloudSignalPWA({
// ...config
iosInstallBanner: {
enabled: true,
appName: 'My App',
showOnFirstVisit: true
}
})
`Problem: Push notifications not working on iOS
Requirements:
- iOS 16.4+
- PWA must be installed to home screen
- User must grant permission after installation
$3
Problem: 401 errors after token expires
Solution: Implement
onTokenExpired callback:
`javascript
const pwa = new CloudSignalPWA({
// ...config
onTokenExpired: async () => {
// Your token refresh logic
const newToken = await refreshMyToken()
return newToken
}
})
`Problem: Registration not linked to user after login
Solution: Call
setUserToken() followed by registerForPush():
`javascript
pwa.setUserToken(newJWT)
await pwa.registerForPush() // Re-registers with user identity
`$3
Enable debug logging to troubleshoot issues:
`javascript
const pwa = new CloudSignalPWA({
// ...config
debug: true // Logs all SDK operations to console
})
`Production Checklist
Before deploying to production, verify:
- [ ] HTTPS enabled - Required for service workers and push notifications
- [ ] Service worker at root -
/service-worker.js or /public/service-worker.js
- [ ] Manifest.json configured - Icons, name, start_url, display mode
- [ ] Environment variables set - Organization ID, Service ID, Secret/JWT config
- [ ] VAPID keys configured - In CloudSignal dashboard
- [ ] Test on target devices - iOS Safari, Chrome, Firefox, Edge
- [ ] Test install flow - Both automatic prompt and iOS manual instructions
- [ ] Test push notifications - Send test notification from dashboard
- [ ] Test offline behavior - Verify offline queue and reconnection
- [ ] Token refresh tested - If using JWT, verify refresh flow works
- [ ] Error handling - Graceful degradation when features unavailableMigration from Other Services
$3
CloudSignal uses Web Push directly (not FCM). Key differences:
- No Firebase SDK dependency
- VAPID keys instead of FCM server key
- Direct Web Push Protocol
`javascript
// Before (FCM)
import { getMessaging, getToken } from 'firebase/messaging'
const token = await getToken(messaging, { vapidKey: '...' })// After (CloudSignal)
import { CloudSignalPWA } from '@cloudsignal/pwa-sdk'
const registration = await pwa.registerForPush()
`$3
Remove OneSignal SDK and replace with CloudSignal:
`javascript
// Before
OneSignal.push(['init', { appId: '...' }])// After
const pwa = new CloudSignalPWA({ organizationId: '...', serviceId: '...' })
await pwa.initialize()
``MIT License - Copyright (c) 2024-2025 CloudSignal