BuildBaseBuildBase

Overview

Add browser push notifications with usePushNotifications, inside the SaaSOSProvider you already have.

BuildBase provides browser push notifications out of the box. SaaSOSProvider (also exported as BuildBaseProvider) already mounts the push provider, so usePushNotifications() works anywhere inside it and there is nothing extra to wrap. The Notifications screen of the built-in settings uses the same provider, so it has its own subscribe button.

What is left to you is serving the service worker and, if you want one, a control of your own:

import { usePushNotifications } from '@buildbase/sdk/react';

function NotificationBell() {
  const {
    isSupported,
    permission,
    isSubscribed,
    loading,
    error,
    subscribe,
    unsubscribe,
  } = usePushNotifications();

  if (!isSupported) return null;
  if (permission === 'denied') return <p>Notifications are blocked.</p>;

  return (
    <>
      <button
        disabled={loading}
        onClick={isSubscribed ? unsubscribe : subscribe}
      >
        {isSubscribed ? 'Turn off notifications' : 'Turn on notifications'}
      </button>
      {error && <p role="alert">{error}</p>}
    </>
  );
}

subscribe() asks for permission itself, so there is no separate requestPermission() call before it. It does nothing for a signed-out visitor: a subscription belongs to a user, so render the control where someone is signed in.

Warning

There is no PushNotificationProvider to add. It reads the org and the signed-in user from SaaSOSProvider, so outside it, it throws (useOSStore must be used within a OSProvider), and inside it there is already one.

Serve the service worker

When a user subscribes, the SDK registers the service worker at /push-sw.js on your origin. The SDK does not host that file. Your app has to serve it from that path, or subscribe() fails when the browser cannot fetch the worker.

The worker script ships in the SDK as the PUSH_SERVICE_WORKER_SCRIPT string, exported from both @buildbase/sdk and @buildbase/sdk/react. Write it into your public directory at build time, so it updates with the SDK:

// scripts/write-push-sw.mjs
import fs from 'node:fs';
import { PUSH_SERVICE_WORKER_SCRIPT } from '@buildbase/sdk';

fs.writeFileSync('public/push-sw.js', PUSH_SERVICE_WORKER_SCRIPT);
{
  "scripts": {
    "predev": "node scripts/write-push-sw.mjs",
    "prebuild": "node scripts/write-push-sw.mjs"
  }
}

Note

Rich notifications need @buildbase/sdk 0.0.83 or later, and a push-sw.js written from PUSH_SERVICE_WORKER_SCRIPT at build time, as above. That worker is what shows image, badge, action buttons, tag replacement, requireInteraction, silent, renotify, timestamp and dir. The 0.0.82 worker still shows the title, body and icon and opens the tracked link, and ignores the rest. A copy pasted into public/ by hand stays the version you copied: upgrading the SDK does not change it.

Serving it from another path

The path is fixed at /push-sw.js. SaaSOSProvider mounts its push provider with no options and has no prop to change that, and the settings screen and every usePushNotifications() call under it register and look up that path.

To keep the file somewhere else, answer /push-sw.js with it through a rewrite in your framework or host. Not a redirect: browsers refuse a worker script that redirects.

A second PushNotificationProvider nested inside SaaSOSProvider with its own serviceWorkerPath changes only the usePushNotifications() calls below it. The settings screen keeps /push-sw.js, so it can show a different state from your control, and its button registers the default worker, not yours. If you nest one, hide that block with ui={{ settings: { notifications: { push: false } } }} on SaaSOSProvider, so your control is the only one. The provider's autoSubscribe prop is accepted and does nothing.

usePushNotifications() returns

ReturnTypeDescription
permission'default' / 'granted' / 'denied'Browser notification permission
isSubscribedbooleanWhether this browser holds a push subscription. Checked once a user is signed in, and false until that check finishes
isSupportedbooleanWhether the browser supports service workers, push and notifications
loadingbooleantrue while subscribe() or unsubscribe() runs
errorstring | nullWhy the last subscribe() or unsubscribe() failed, including a refused permission. Cleared when the next one starts
requestPermission()() => Promise<boolean>Ask for notification permission without subscribing. Resolves true when granted
subscribe()() => Promise<void>Ask for permission if needed, subscribe this browser and register it with BuildBase. Does not reject: a failure sets error
unsubscribe()() => Promise<void>Unsubscribe this browser and remove it from BuildBase. Does not reject: a failure sets error

Before it sends

In production, each of these has to hold before a notification reaches a browser:

  • An HTTPS origin serving /push-sw.js. Browsers only register service workers and subscribe to push on a secure origin. localhost counts while you develop.
  • A browser push service. The server only accepts a subscription whose endpoint belongs to the push services run by Google, Mozilla, Microsoft and Apple, the ones Chrome, Firefox, Edge and Safari subscribe through. Any other endpoint is refused and never stored, since the server would be calling it on every send.
  • A plan that includes push notifications. On a plan without them nothing is pushed, though the inbox item, and the email if the event has a template, still go. If the org has no VAPID keys yet, subscribe() also fails, with "Push notifications are not available right now."
  • Push switched on for the org and the event. The org switch is Push notifications under Messaging > Notification events > Global in the console. Off, campaigns are refused and your events send no push. Each event has its own push channel, and an event registered by its first send() starts with push on. Members' own preferences come after that - see the four gates.
  • VAPID keys. Every push is signed with a key pair that belongs to the org. Generate it under Messaging > Push campaigns, or let the first browser that subscribes create it. Until it exists, sends skip push.
  • Room under the plan's push subscriber limit. At the limit a new browser is turned away: subscribe() fails and error, like the settings screen, reads "Push notifications are not available right now." It names no plan, because the person reading it is your user. Browsers already subscribed keep receiving, and you are told through the organization.end_user_ceiling_reached webhook event.
  • Push left in the plan's allowance. Every device a push is delivered to counts against the plan's push allowance for the period. On a plan that allows no overage, once what is left cannot cover a person's devices, their push is skipped and the inbox item still goes. You are told through the same organization.end_user_ceiling_reached event.

Each time a browser subscribes, it gets one "Notifications Enabled" notification straight away, which is the quickest way to see the whole path work.

Sending notifications (server-side)

Use the server SDK to send notifications to users:

import BuildBase from '@buildbase/sdk';

const bb = BuildBase({ serverUrl, orgId, getSessionId });

// Send to a specific user
await bb.notification.send(workspaceId, 'comment-added', userId, {
  title: 'New comment',
  message: 'Someone commented on your post.',
  url: '/posts/123',
});

// Send to all workspace members
await bb.notification.send(workspaceId, 'new-release');

The rich fields (image, badge, tag, actions and the rest) go in the same data object, and the exported NotificationData type has every one of them. Notifications explains the common ones, along with the gates every send passes.

Next Steps

  • Notifications - One call for email and push, and the gates it passes.
  • Webhooks - React to events server-side.
  • Server SDK - All server methods including notifications.