Service Worker
The Service Worker is what turns a push message into a visible notification, even when your page is closed. It must be served from your own origin — no CDN shortcut exists, and none is claimed here.
Hosting requirements
| Requirement | Why |
|---|---|
| Same origin as the page | A worker script served from another origin cannot control your pages; the browser refuses to register it. |
HTTPS (or http://localhost) | Service Workers require a secure context. |
| Path covers the scope | By default a worker at /js/sw.js can only control /js/*. To control the whole site, serve it from the root or send Service-Worker-Allowed: /. |
| JavaScript MIME type | text/javascript or application/javascript. |
| No aggressive caching | A stale worker can pin an old payload format; serve with a short Cache-Control. |
Installing it
# copy both files to your web root
cp sdk/service-worker.js /var/www/example.com/service-worker.js
cp sdk/creator-notify.js /var/www/example.com/creator-notify.js
The SDK registers it from serviceWorkerPath (default /service-worker.js):
const notify = new CreatorNotify({
websiteId: 'pub_8d1f0c...',
apiBaseUrl: 'https://YOUR-API-HOST',
serviceWorkerPath: '/service-worker.js',
serviceWorkerScope: '/'
});
If you must keep the file elsewhere, send the header from your web server:
# nginx
location = /assets/service-worker.js {
add_header Service-Worker-Allowed /;
add_header Cache-Control "public, max-age=300";
types { } default_type application/javascript;
}
# Apache (.htaccess)
<Files "service-worker.js">
Header set Service-Worker-Allowed "/"
Header set Cache-Control "public, max-age=300"
</Files>
Creator Notify also serves a copy
GET /sdk/service-worker.js returns the same file with Service-Worker-Allowed: /. Use
it to test quickly, but for production copy the file to your own domain — registration is
origin-scoped.
What the worker does
push
- Parses the JSON payload. If it is not JSON, the raw text becomes the notification body. If there is no payload at all, a neutral fallback is shown.
- Copies
title,body,icon,badge,image,tagand up to twoactions. - Validates every URL against an allow-list of origins (the site's own origin by default, extended through the SDK's configuration message).
- Retries with a minimal option set if the platform rejects an option it does not support (for example
imageon some Android builds).
notificationclick
- Closes the notification and reads the validated destination from
notification.data.url. - Finds a window client on the same origin: the exact URL is navigated and focused; otherwise the first client is focused.
- Falls back to
clients.openWindow(url)when no window is open. - Action buttons may carry their own destination through
data.actions[].url.
pushsubscriptionchange
Browsers may rotate a subscription without asking. The worker cannot re-subscribe without the VAPID key and a
user gesture, so it notifies the page. The SDK surfaces this as a SUBSCRIPTION_CHANGED error; call
subscribe() again from a click to restore delivery.
Controlling click destinations
{
"title": "Order shipped",
"body": "Tracking number 1Z999",
"url": "https://example.com/orders/1234",
"data": { "orderId": 1234 }
}
The worker only opens URLs on an allowed origin. Relative values are resolved against the site origin before
validation. Anything that is not http: or https: is discarded, so
javascript: and data: destinations cannot be injected.
Browser support and differences
| Browser | Background push | Notes |
|---|---|---|
| Chrome / Edge (desktop, Android) | Yes, via FCM | Requires a user gesture for the permission prompt. image is supported on Android, not on desktop. |
| Firefox (desktop, Android) | Yes, via Mozilla autopush | Uses its own push service; endpoints differ. Some actions are unsupported. |
| Safari 16.4+ (macOS, iOS) | Yes, via APNs | On iOS the site must be added to the Home Screen as a web app. Notification options are a subset. |
| Older Safari / IE | No | isSupported() returns false; hide the button. |
| Private / incognito windows | Varies | Subscriptions often die with the session — expect churn. |
| Browsers in "quiet" notification mode | Delivered but not surfaced | The push service accepted the message; this is not a Creator Notify failure. |
No cross-browser guarantee
Push behaviour — payload size limits, action support, image rendering, notification grouping, badge colours —
differs per platform and OS version. Test on the browsers you actually care about. The payload limits enforced
by Creator Notify (MAX_PUSH_PAYLOAD_BYTES, default 3500) are chosen to be accepted by all current
push services, but rendering remains the browser's decision.
Permissions
- The prompt can only appear after a user gesture on Chrome; Firefox and Safari allow it but punish unsolicited prompts with quieter UI or blocking.
- A denied permission is sticky per origin — the user must change it in browser settings.
getPermission()returns'denied'immediately; do not re-prompt in a loop.- The SDK never calls
requestPermission()at load time.
Debugging
# Chrome: chrome://serviceworker-internals — unregister, push, inspect
# Firefox: about:debugging#/runtime/this-firefox → Service Workers
# Safari: Develop → Service Workers, and the Web Inspector console
// From the page console, after registration:
const reg = await navigator.serviceWorker.getRegistration('/');
await reg.pushManager.getSubscription();
A push that the push service accepted but never rendered is usually one of: permission revoked, the site muted at OS level, a browser-level "quiet" mode, or a payload the platform refused. See Troubleshooting.