Service Workers in Node Context¶
Wait - Service Workers are a browser API You use them in Progressive Web Apps to intercept network requests , cache assets , and enable offline mode In Node.js , we don't have a DOM , we don't have fetch interception in the same way , and we certainly don't have a browser So what does "PWA" mean for a Node developer?
Two things: building the backend for PWAs , and building desktop apps with Node
The Node.js Role in PWAs¶
A PWA needs a backend - API endpoints , push notifications , authentication Node.js is the most common backend for PWAs because it handles Web Push , WebSocket connections , and high-concurrency API requests efficiently
// PWA push notification endpoint
const webpush = require('web-push')
webpush.setVapidDetails(
'mailto:admin@myapp.com',
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
)
app.post('/api/subscribe', (req, res) => {
const subscription = req.body
// Store subscription in database for later use
const payload = JSON.stringify({
title: 'Welcome!',
body: 'Notifications enabled',
icon: '/icon.png'
})
webpush.sendNotification(subscription, payload)
.then(() => res.json({ ok: true }))
.catch(err => {
// Subscription expired or invalid - remove from DB
res.status(410).json({ error: 'subscription expired' })
})
})
PWAs need HTTPS (required for Service Workers) , push endpoints , and service worker files served from your Node server
// Serve service worker from Node
app.get('/sw.js', (req, res) => {
res.set('Content-Type', 'application/javascript')
res.set('Service-Worker-Allowed', '/')
res.sendFile(path.join(__dirname, 'public', 'sw.js'))
})
// Cache-first strategy - serve from cache, update in background
// This goes in sw.js (browser-side), served by Node
Electron - Node.js + Chromium¶
Electron wraps Node.js and Chromium into a desktop application Your app has access to the full Node.js API (file system , native addons , child processes) AND the full browser API (DOM , Canvas , WebGL , Service Workers)
// main.js - Electron main process
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
let mainWindow
app.whenReady().then(() => {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false // security: don't expose Node in renderer
}
})
mainWindow.loadFile('index.html')
})
// IPC - secure communication between main and renderer
ipcMain.handle('read-file', async (event, filePath) => {
const fs = require('fs/promises')
try {
const content = await fs.readFile(filePath, 'utf-8')
return { success: true, content }
} catch (err) {
return { success: false, error: err.message }
}
})
// preload.js - bridge between main process and renderer
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
readFile: (path) => ipcRenderer.invoke('read-file', path),
getPlatform: () => process.platform,
getVersion: () => process.versions.node
})
// renderer.js - runs in browser context (no direct Node access)
const { readFile } = window.electronAPI
async function loadConfig() {
const result = await readFile('/etc/config.json')
if (result.success) {
document.getElementById('config').textContent = result.content
}
}
contextIsolation: true is non-negotiable Never disable contextIsolation - it prevents the renderer from accessing Node.js APIs directly The preload script is your security boundary - expose only what the renderer needs
Building a PWA Backend with Node¶
// Complete PWA backend stack
const express = require('express')
const webpush = require('web-push')
const sqlite3 = require('better-sqlite3')
const app = express()
const db = new sqlite3('subscriptions.db')
app.use(express.json())
// Serve static PWA files
app.use(express.static('public'))
// Register push subscription
app.post('/api/push/subscribe', (req, res) => {
const { endpoint, keys } = req.body
const stmt = db.prepare(
'INSERT OR REPLACE INTO subscriptions (endpoint, p256dh, auth) VALUES (?, ?, ?)'
)
stmt.run(endpoint, keys.p256dh, keys.auth)
res.json({ ok: true })
})
// Send push notification to all subscribers
app.post('/api/push/send', async (req, res) => {
const { title, body } = req.body
const rows = db.prepare('SELECT * FROM subscriptions').all()
const results = await Promise.allSettled(
rows(sub => webpush.sendNotification({
endpoint: sub.endpoint,
keys: { p256dh: sub.p256dh, auth: sub.auth }
}, JSON.stringify({ title, body })))
)
// Remove invalid subscriptions
results.forEach((result, i) => {
if (result.status === 'rejected' && result.reason.statusCode === 410) {
db.prepare('DELETE FROM subscriptions WHERE endpoint = ?')
.run(rows[i].endpoint)
}
})
res.json({ sent: results.filter(r => r.status === 'fulfilled').length })
})
// Push notification - not a PWA without HTTPS
// Use Let's Encrypt + certbot for TLS
app.listen(443, () => console.log('PWA backend running'))
PWA checklist for Node backend: - HTTPS with valid TLS certificate - Push notification endpoints (VAPID keys) - Service worker file served with correct MIME type - Manifest.json with app metadata - Offline support via Cache API (browser-side)
Security in Electron/Desktop Apps¶
// DANGER - never disable webSecurity
const win = new BrowserWindow({
webPreferences: {
webSecurity: false, // XSS becomes RCE - NEVER
nodeIntegration: true, // renderer gets full Node access - NEVER
contextIsolation: false // renderer shares scope - NEVER
}
})
Electron apps bundle both JavaScript engines - if an attacker XSSes your renderer , they have access to everything the preload script exposes
// SECURE - minimal preload bridge
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('api', {
getData: () => ipcRenderer.invoke('get-data'),
saveFile: (name, content) => ipcRenderer.invoke('save-file', name, content)
})
Validate all IPC inputs on the main process side The main process has full Node.js access - don't let the renderer trick it into deleting files
Prerequisites¶
- adv_03_native_addons.md - native addons used in Electron apps
next -> adv_05_cli_apps.md