Highlight
Declarative Web Push adds a
web_push: 8030marker key to the push message. The browser shows the notification directly, without running any Service Worker JavaScript, and stays backwards compatible with older browsers.
Core Content
A native app never has to run code on the device to show a push notification. The notification is described in the push message in a standard format, and the system displays it on receipt. Even an app offloaded to the cloud by iOS still pops up its notifications correctly. The web takes a detour: every push first wakes a Service Worker, the developer’s JavaScript parses the JSON, and then calls showNotification(). That means an extra block of code to maintain, and extra CPU and battery. Worse, Intelligent Tracking Prevention limits the lifetime of Service Worker JavaScript for privacy reasons, and pushes sometimes never reach the user at all.
The WebKit team noticed something: a lot of sites’ push messages are just a small piece of JSON, and the Service Worker logic is just translating fields into arguments for showNotification(). Since this is a common pattern, hand the work to the browser. The core of Declarative Web Push is one convention: include "web_push": 8030 in the push message, and the browser treats the whole JSON as a standard notification description and displays it automatically, without starting the Service Worker. Safari 18.5 (macOS) and “Add to Home Screen” web apps starting with iOS 18.4 / iPadOS 18.4 already support it.
Backwards compatibility is the most comfortable part of the design. If the browser does not recognize the magic key, it falls back to original Web Push. If the JSON fails to parse, it falls back. If the JSON is valid but does not describe a usable notification, the browser drops it. If you really need end-to-end decryption on the client or need to fix up an unread count, add "mutable": true to the JSON and let the Service Worker do an optional post-processing step. If that step fails, the browser falls back to the plaintext notification in the declarative JSON.
Detailed Content
The original Web Push flow depends on the Service Worker: register the worker, request a subscription through pushManager, and when a push arrives the worker receives a push event and calls showNotification (04:41). Declarative Web Push cuts every line of code except the subscription request. The subscription entry point also moves from ServiceWorkerRegistration.pushManager up to window.pushManager, since a Service Worker is no longer required (07:54).
A minimal declarative push message looks like this (08:14):
{
"web_push": 8030,
"notification": {
"title": "Hello from Browser Pets",
"navigate": "https://browserpets.example/inbox"
}
}
Key points:
"web_push": 8030: the magic key, fixed at 8030 (the RFC number for IETF Web Push). The browser uses this key to decide whether to take the declarative path; if it is missing, the browser falls back to original Web Push.notification: required, describes the user-visible notification.title: notification title, required.navigate: the URL to open when the user taps the notification, required. This is the smallest valid form of a declarative notification.
The full field set reuses the W3C NotificationOptions dictionary directly, so body, tag, sound, and app badge all fit in (10:08):
{
"web_push": 8030,
"notification": {
"title": "Hello from Browser Pets",
"navigate": "https://browserpets.example/inbox",
"body": "You have a new direct message",
"tag": "dm",
"silent": false
},
"app_badge": "3"
}
Key points:
body: notification body, same meaning asNotification.options.body.tag: identifier for merging or replacing notifications, synonymous with the nativeNotificationOptions.tag.silent: false: asks the platform to play the default notification sound, subject to system policy.app_badge: the declarative push has a built-in app badge update, removing the separateBadging APIcall from the Service Worker.
When you need client-side decryption or content fixup, add "mutable": true, and the browser dispatches the push to the Service Worker with a proposed notification attached to the event (12:45):
self.addEventListener("push", (event) => {
const proposed = event.proposedNotification;
const data = event.data.json();
const decrypted = tryDecrypt(data.encrypted);
if (decrypted) {
event.waitUntil(
self.registration.showNotification(decrypted.title, decrypted.options)
);
}
// If showNotification is not called, the browser falls back to proposedNotification
});
Key points:
event.proposedNotification: the “original proposal” from the declarative JSON inside the mutable flow. The Service Worker can read it to decide, for example, whether this is a DM.tryDecrypt(...): locally decrypts the encrypted payload in the push using the key; only devices that hold the key can read the plaintext.showNotification(...): replaces the proposal with the real text after a successful decrypt; the browser shows the replacement.- Skipping
showNotification: when decryption fails, the worker fails to start, or resources are tight, the browser falls back to the plaintext notification in the declarative JSON (14:01).
The Browser Pets migration example shows the upgrade path (15:36): rename the ad-hoc JSON fields built up over time (title, clickURL, body, silent …) one by one to the standard NotificationOptions fields, add the magic key, and new browsers immediately take the declarative path. Old browsers still go through the Service Worker, but the worker code shrinks to showNotification(data.notification.title, data.notification) and no longer needs hand-written parsing of data.clickURL and friends.
Core Takeaways
-
What to do: rename your existing ad-hoc push JSON to the standard
NotificationOptionsfields and add"web_push": 8030.- Why it is worth doing: new browsers take the declarative path immediately, saving one Service Worker startup, using less power, and being less likely to be cut off by ITP; old browsers fall back automatically, with zero risk.
- How to start: first do a field mapping in the server’s push message builder (such as
clickURL → navigate,text → body), then inject the magic key at the top of the JSON.
-
What to do: shrink the Service Worker push handler to “take the
notificationfield and forward it toshowNotification”.- Why it is worth doing: the
notificationsub-object in the declarative JSON is itself a validNotificationOptions, so no field stitching is needed and the bug surface drops sharply. - How to start: in the
pushevent, readevent.data.json().notification, pull out thetitle, and pass the rest of the object as options toshowNotification.
- Why it is worth doing: the
-
What to do: only set
"mutable": truefor cases that must be handled locally, like end-to-end encryption or unread-count fixup.- Why it is worth doing: letting the browser display the notification by default keeps your notifications flowing even when the Service Worker has been cleaned up by ITP or the device is under load; only fall back to the mutable flow when needed.
- How to start: take stock of your current push types, mark “display only” and “needs local decryption or fixup” separately, make the first kind fully declarative, and keep mutable plus Service Worker fallback for the second.
-
What to do: use the
app_badgefield to merge the app badge update into the same push as the notification.- Why it is worth doing: it removes one worker wakeup and one Badging API call, and keeps the badge and the notification atomically consistent.
- How to start: when assembling the push on the server, write the unread count straight into
app_badgeinstead of sending a separate empty push to update the badge.
Related Sessions
- Meet WebKit for SwiftUI — embed and control web content in SwiftUI with WebKit
- What’s new in Safari and WebKit — overview of this year’s new Safari and WebKit capabilities, including Web Push progress
- What’s new in web apps — new features for Home Screen web apps, the host for declarative push on iOS
Comments
GitHub Issues · utterances