Secure a PWA
Published Updated
A PWA uses a secure context for powerful APIs, a Content Security Policy (CSP) for resource loading, and an intentional service worker scope for request control. Configure and check these boundaries during deployment.
Use window.isSecureContext to check the active context and test for serviceWorker in navigator before registration. CSP violations can be observed in the document with ReportingObserver or sent to an endpoint configured with reporting headers and directives (Secure contexts, MDN; Content-Security-Policy, MDN).
Where it is supported
Section titled “Where it is supported”MDN describes ServiceWorkerContainer.register() as widely available across browsers since April 2018 (ServiceWorkerContainer.register(), MDN). MDN marks the CSP response header as widely available, although support for individual directives varies (Content-Security-Policy, MDN).
Serve the application in a secure context
Section titled “Serve the application in a secure context”MDN defines potentially trustworthy origins to include HTTPS and local development origins such as http://localhost. Features restricted to secure contexts are unavailable when the active document is not trustworthy. Redirect production HTTP traffic to HTTPS, and check the runtime property before starting a feature that requires a secure context.
Detect the deployment boundary
Section titled “Detect the deployment boundary”The fallback keeps navigation and online content available while skipping service worker registration.
const status = document.createElement('output');status.id = 'offline-status';status.setAttribute('aria-live', 'polite');document.body.append(status);
if (!window.isSecureContext) { status.textContent = 'Offline installation is unavailable on this connection.';} else if (!('serviceWorker' in navigator)) { status.textContent = 'This browser runs the online experience.';} else { navigator.serviceWorker.register('/sw.js', { scope: '/' }) .then(() => { status.textContent = 'Service worker registration was created or updated.'; }) .catch(() => { status.textContent = 'The online experience remains available.'; });}The explicit branches make a failed security or capability check visible without blocking the page.
Deliver CSP as a response header
Section titled “Deliver CSP as a response header”The Content-Security-Policy response header lets a site restrict resources such as scripts, styles, images, frames, and connections. MDN’s report-only example uses Content-Security-Policy-Report-Only during testing to report violations without blocking execution.
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'This example limits default and script loads to the same origin, blocks plugins, restricts <base> URLs, and prevents embedding. Adapt the policy to resources the application actually loads.
Limit service worker scope
Section titled “Limit service worker scope”The default registration scope is the directory containing the service worker script. To request a broader scope than that directory, the service worker script response must supply a Service-Worker-Allowed header that permits it (ServiceWorkerContainer.register(), MDN; Service worker script response, W3C).
Place /app/sw.js under /app/ and register it with { scope: '/app/' } when it should control only that path. Avoid overlapping registrations: MDN warns that overlapping scopes are generally safer to avoid because only one registration controls a document.
Verify the deployed boundaries
Section titled “Verify the deployed boundaries”Inspect the final response, not only source files. Confirm that the page is a secure context, the CSP header is present on the document response, and registration.scope matches the intended URL boundary. Observe CSP violations with ReportingObserver, or send reports to an endpoint configured by the policy’s reporting directives (Content-Security-Policy, MDN).