Skip to content

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 (localhost is 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_enabled and the SDK reports not_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 ​

bash
npm install notti-web-sdk

The 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.js covers the whole site, while /assets/notti-sw.js covers only /assets/. To serve it from a subpath and still cover the site, send the header Service-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 the serviceWorkerPath option 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:

js
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:

reasonMeaning
insecure_contextThe page is not served over https (or localhost).
ios_requires_home_screeniOS or iPadOS browser tab: the site must be added to the Home Screen first.
no_service_workerThe browser has no Service Worker support.
no_push_managerThe browser has no Push API or no Notification.
not_enabledWeb push is not enabled for this App.
request_failedThe public key could not be loaded (network, bad key, server error).
invalid_optionsappId, 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:

js
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:

js
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();
  • identify links the browser to your own user id, so a push addressed to external_user_ids reaches it. An empty id does nothing.
  • setTags merges into the tags already set and sends the full resulting map. Values are strings, numbers or booleans; anything else is ignored.
  • logout unlinks 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 received once for each notification it showed, clicked and opened when 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:

json
{
  "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 https URL on another origin is opened in a new window and never focuses an existing one.
  • No url, or a URL that is not http(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 as payload_too_large without 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 image is 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, Urgency and TTL headers of the push request.
  • Gone browsers. When a push service answers 404 or 410 the visitor unsubscribed or revoked permission: the Delivery fails permanently and the Device stops being a target.
  • Throttling. When a push service answers 429 with Retry-After, Notti waits that long (up to 5 minutes) before trying again, and gives up after about 20 tries.

Browser support ​

BrowserStatus
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 laterExpected to work, not validated yet. Permission must come from a user gesture.
Safari on iOS and iPadOS 16.4 or laterExpected 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:

json
{
  "name": "My site",
  "start_url": "/",
  "display": "standalone",
  "icons": [{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" }]
}
html
<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 https from your visitors' browsers, and your site must be https too.
  • The server sends each push to the browser vendor's push service, so it needs outbound https. Known hosts are fcm.googleapis.com, updates.push.services.mozilla.com, *.push.apple.com and *.notify.windows.com; Notti does not keep an allow-list, because vendors change. It only follows https endpoints 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 Authorization header, never with a cookie. Put a proxy in front of Notti only if it forwards OPTIONS requests and these headers.
  • If Notti answers 403 from 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, 5xx and Apple's 403 reasons are documented. Notti treats a 401 or 403 from 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.

  • pushsubscriptionchange on iOS. Browsers differ on whether it fires. The SDK also re-checks the subscription on every init, 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 404 or 410.

  • The SDK has no script-tag (CDN) build; use a bundler.