Overview
The Notti Web SDK registers a visitor's browser with Notti and ships the service worker that shows the notification and reports engagement (received / opened / clicked). A web push is sent like any other push: from the dashboard, the API or MCP, to a segment, to external user ids, to device ids or to all devices, and web is one more platform next to ios and android. It uses the Web Push protocol (RFC 8030) with VAPID (RFC 8292) and aes128gcm payload encryption (RFC 8291), so Notti talks to the browser vendor's push service and no Firebase project is involved.
The SDK is a TypeScript package with no dependencies. It lives at zeeplabs/zeep-notti-web and is published on npm as notti-web-sdk. It is in 0.x, so the API can still change between minor versions.
By default the SDK talks to Notti (https://app.zeepnotti.app), so Notti.init only needs your app id and client key.
Requirements
- A site served over
https(localhostis allowed for development). Browsers only expose service workers and push in a secure context. - A Notti App with web push enabled (next section) and its app id and client key. Never put the REST key in a web page.
- A Notti backend that includes web push. An older backend answers
404 web_push_not_enabledand the SDK reportsnot_enabled. - A browser with Service Workers and the Push API: Chrome, Edge and Firefox on desktop and Android, Safari 16 or later on macOS, and Safari on iOS and iPadOS 16.4 or later only after the site is added to the Home Screen (see Browser support).
Enable web push for the App
In the dashboard open Settings → Push & In-App → Web and enable web push. Only owners and developers can. Notti creates a VAPID key pair once and the browser SDK reads the public key; the private key never leaves Notti. Set the subject to a mailto: or https: address you control: push services use it to reach you if your pushes misbehave. The subject is required and Notti never fills it in for you, because it is sent to Apple, Google and Mozilla with every push: prefer a role mailbox over a person's address.
Disabling web push keeps the key pair and the registered browsers, but pushes to them fail until you enable it again. There is no key rotation in the dashboard yet. If a key changes in the future, the SDK notices on the next init and registers a new subscription without asking the visitor again.
Installation
npm install notti-web-sdkThe package ships the SDK (ES module, CommonJS and types) and the service worker dist/notti-sw.js, also exported as notti-web-sdk/sw. You copy the worker from node_modules/notti-web-sdk/dist/notti-sw.js into your site in the next section. To build from source instead, clone the repository and run pnpm install && pnpm build.
Serve the service worker
Browsers require the worker to be a file on your own origin. Copy dist/notti-sw.js into your site so it is served as /notti-sw.js:
- Serve it from the root. A worker controls the pages at its path and below, so
/notti-sw.jscovers the whole site, while/assets/notti-sw.jscovers only/assets/. To serve it from a subpath and still cover the site, send the headerService-Worker-Allowed: /. - Use the file that ships with the same SDK version you import. The worker carries a version string and the SDK compares it on every registration: when they differ the SDK asks the browser to fetch the file again and logs a console warning, because your host may still be serving an old copy.
- Serve it as JavaScript and avoid long caching (
Cache-Control: no-cache), so a new version reaches visitors. - Do not wrap, minify further or edit it. If your site already has a service worker, load Notti's from yours with
importScripts('/notti-sw.js')and set theserviceWorkerPathoption to your own worker, so the SDK registers yours and not Notti's file (which would replace it). Keep both files on the same origin. A registration has a single push subscription, so the push service is the one this SDK subscribed to. From SDK 0.1.1 the Notti worker acts only on notifications that carry Notti's ids and leaves your own notifications alone.
Initialize
Call init when the page loads. It never prompts and never throws:
import { Notti } from 'notti-web-sdk';
const { supported, reason } = await Notti.init({
appId: 'YOUR_APP_ID',
clientKey: 'YOUR_CLIENT_KEY',
host: 'https://notti.example.com', // optional, defaults to https://app.zeepnotti.app
serviceWorkerPath: '/notti-sw.js', // optional, this is the default
});init checks the browser first, then loads the App's public key. When supported is false, reason says why:
reason | Meaning |
|---|---|
insecure_context | The page is not served over https (or localhost). |
ios_requires_home_screen | iOS or iPadOS browser tab: the site must be added to the Home Screen first. |
no_service_worker | The browser has no Service Worker support. |
no_push_manager | The browser has no Push API or no Notification. |
not_enabled | Web push is not enabled for this App. |
request_failed | The public key could not be loaded (network, bad key, server error). |
invalid_options | appId, clientKey or host is missing or invalid. |
Use the reason to decide what your UI shows, for example a hint to install the site on iOS.
If the visitor already enabled notifications through the SDK and permission is still granted, init also re-checks the browser subscription and registers a new one when the endpoint or the App's key changed. It never prompts and never subscribes a visitor who did not opt in.
Ask for permission
Call requestPermission from a user gesture, such as a click. Browsers ignore or block the prompt otherwise, and Safari requires it:
button.addEventListener('click', async () => {
const result = await Notti.requestPermission();
// { permission: 'granted' | 'denied' | 'default', registered: boolean, deviceId?, reason? }
});When the visitor grants it, the SDK creates (or reuses) the push subscription and registers one Device with platform: "web". Calling it again, or reloading the page, does not create a duplicate. When the visitor denies or dismisses the prompt, nothing is registered and the SDK never asks again on its own.
If registered is false although permission is granted, reason is not_initialized (call init first), not_enabled, subscribe_failed (the browser refused or the worker was not ready) or register_failed (Notti could not be reached; the SDK retries at the next init).
Notti.getSubscription() returns { deviceId, endpoint } for the registered browser, or null.
Identity and tags
These behave like the mobile SDKs:
await Notti.identify('user-42');
await Notti.setTags({ plan: 'pro', beta: true });
await Notti.setTags({ beta: null }); // null removes a key
await Notti.removeTags(['plan']);
await Notti.logout();identifylinks the browser to your own user id, so a push addressed toexternal_user_idsreaches it. An empty id does nothing.setTagsmerges into the tags already set and sends the full resulting map. Values are strings, numbers or booleans; anything else is ignored.logoutunlinks the browser from the user (external_user_id: null). Tags and the subscription are kept.- Calls made before the browser is registered are kept and sent with the registration. A call that fails is retried at the next
init. None of these methods throws.
The user id is set by the browser, not verified. The Client key is public, so anyone who has it can register a browser under any external_user_id, and would then receive the pushes you send to that user id. Do not put anything in a push that only its user should read, or send it to device ids instead. Registrations made with a Client key are limited to 120 per minute for each client IP. Past that the API answers 429, and the SDK tries again at the next init.
What the service worker does
- Shows every push. The worker always calls
showNotification, even for a payload it cannot read (it then shows a generic notification titled with your site's hostname). This is deliberate: a browser can revoke a site's permission if it receives a push and shows nothing. - Reports engagement. It reports
receivedonce for each notification it showed,clickedandopenedwhen the visitor clicks. Dismissing a notification reports nothing. Event requests are retried a few times and then dropped, and never delay the notification. - Opens the click target. See below.
- Re-registers on
pushsubscriptionchange. When the browser replaces the subscription and permission is still granted, the worker registers the new one.
Click target
The click target is the url key of the notification's data:
{
"title": "Spring sale",
"body": "20% off today",
"data": { "url": "/offers" }
}- A path (
/offers) opens on your site. If a window is already on that page it is focused, otherwise a new one is opened. - An absolute
httpsURL on another origin is opened in a new window and never focuses an existing one. - No
url, or a URL that is nothttp(s), opens your site's root.
Limits and what is not supported
- Payload size. Web Push allows 4096 bytes in total, which leaves 3993 bytes for the message Notti builds from title, body, image URL, tag and
data. A larger one fails that Delivery aspayload_too_largewithout a retry and does not affect iOS or Android Deliveries of the same notification. - Data-only notifications. A notification with neither title nor body is skipped for web Devices (
web_data_only_unsupported). Silent push does not exist on the web: the browser must show something for every push. - Image. The
imageis passed to the browser and ignored where it is not supported. Safari does not show it. - Collapse and priority. The collapse id, priority and TTL of a notification map to the
Topic,UrgencyandTTLheaders of the push request. - Gone browsers. When a push service answers
404or410the visitor unsubscribed or revoked permission: the Delivery fails permanently and the Device stops being a target. - Throttling. When a push service answers
429withRetry-After, Notti waits that long (up to 5 minutes) before trying again, and gives up after about 20 tries.
Browser support
| Browser | Status |
|---|---|
| Chrome (desktop) | Validated against a real push service. |
| Edge, Firefox (desktop) | Expected to work, not validated yet. |
| Chrome, Firefox (Android) | Expected to work, not validated yet. |
| Safari on macOS 13 (Safari 16) or later | Expected to work, not validated yet. Permission must come from a user gesture. |
| Safari on iOS and iPadOS 16.4 or later | Expected to work only as a Home Screen web app, not validated yet. |
"Expected to work" means the browser implements the Web Push standard that Notti follows; Notti has not verified it on that browser. See Known limitations.
iOS and iPadOS. Push only works for a site the visitor added to the Home Screen, and the site needs a web app manifest whose display is standalone or fullscreen:
{
"name": "My site",
"start_url": "/",
"display": "standalone",
"icons": [{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" }]
}<link rel="manifest" href="/manifest.webmanifest" />In a normal browser tab init returns ios_requires_home_screen; show the visitor how to use Share → Add to Home Screen and ask for permission from inside the installed app. Safari revokes the permission when a push is not shown at once, which is one more reason the worker always shows it.
Self-hosted notes
- Your Notti instance must be reachable over
httpsfrom your visitors' browsers, and your site must behttpstoo. - The server sends each push to the browser vendor's push service, so it needs outbound
https. Known hosts arefcm.googleapis.com,updates.push.services.mozilla.com,*.push.apple.comand*.notify.windows.com; Notti does not keep an allow-list, because vendors change. It only followshttpsendpoints on public addresses and never follows redirects, so a firewall that blocks that traffic makes web Deliveries fail. - Browsers call Notti from your site's origin. The routes the SDK uses (public key, device register and update, notification events) answer cross-origin requests for any origin and authenticate with the client key in the
Authorizationheader, never with a cookie. Put a proxy in front of Notti only if it forwardsOPTIONSrequests and these headers. - If Notti answers
403from a push service, the VAPID credentials were rejected; the owner gets the same alert email as for a missing FCM or APNs credential, at most once a day per App.
Known limitations
These points are not confirmed on real devices yet, so treat them as open:
Only Chrome on desktop was validated against a real push service. The other browsers in the table above follow the same standard, but Notti has not verified them yet.
Error codes of FCM and Edge push services. Only
404,410,429,5xxand Apple's403reasons are documented. Notti treats a401or403from another vendor as a credential failure and any unknown answer as retryable.Apple's exact payload limit. Apple says "4 KB"; Notti applies the standard 3993 bytes everywhere.
Focusing or opening a window from a notification click on iOS. Reported bugs exist in Home Screen web apps; behaviour on the current iOS release is to be confirmed. A click is always reported as
clicked.pushsubscriptionchangeon iOS. Browsers differ on whether it fires. The SDK also re-checks the subscription on everyinit, so a change is picked up the next time the visitor opens your site.The old Device row of a replaced subscription stays until a push to it answers
404or410.The SDK has no script-tag (CDN) build; use a bundler.