Creator Notify v1.0.0

JavaScript SDK

sdk/creator-notify.js is a dependency-free browser script (~10 KB unminified) that registers your Service Worker, requests permission, creates a push subscription and uploads it to your Creator Notify deployment. It contains no secret key and can be served as a plain static file.

Installation

Copy the two files from the repository to your site root and load the script with a normal tag:

cp sdk/creator-notify.js  /var/www/example.com/creator-notify.js
cp sdk/service-worker.js  /var/www/example.com/service-worker.js
<script src="/creator-notify.js"></script>

The script exposes a global CreatorNotify constructor and also supports CommonJS (require('./creator-notify.js')) and AMD loaders. There is no npm package and no CDN URL that this project publishes — host the file yourself, or build a CDN release from your own fork.

Initialization

const notify = new CreatorNotify({
  websiteId: 'pub_8d1f0c...',            // required — public identifier from /v1/sites/register
  apiBaseUrl: 'https://YOUR-API-HOST',     // required in practice — no trailing slash needed
  serviceWorkerPath: '/service-worker.js', // default: '/service-worker.js'
  serviceWorkerScope: '/',               // default: '/'
  vapidPublicKey: undefined,             // optional — fetched from the API when omitted
  onSubscribed: (result) => {},           // optional
  onUnsubscribed: (result) => {},         // optional
  onError: (error) => {},                 // optional
  fetchOptions: {}                        // optional — merged into every fetch()
});
OptionTypeDescription
websiteIdstring, requiredPublic identifier. Safe to publish. Grants no send rights.
apiBaseUrlstringOrigin of your deployment. Empty means "same origin as the page".
serviceWorkerPathstringAbsolute path of the Service Worker file on your origin.
serviceWorkerScopestringRegistration scope. '/' requires the worker to be served from the root path or to send Service-Worker-Allowed.
vapidPublicKeystringSkips the bootstrap request to /v1/subscriptions/config.
onSubscribed / onUnsubscribed / onErrorfunctionCallbacks fired in addition to the returned promise.
fetchOptionsobjectMerged into every fetch() call — useful for mode, credentials or custom headers.

init() performs feature detection only. It never prompts the user.

notify.init();

isSupported() and isSecureContext()

if (!notify.isSupported()) {
  // Service Worker, Push API or Notification missing, or the page is not a secure context
}

notify.isSecureContext(); // boolean — HTTPS or localhost

isSupported() checks 'serviceWorker' in navigator, 'PushManager' in window, the presence of Notification and window.isSecureContext.

Permission handling

notify.getPermission();       // 'default' | 'granted' | 'denied' | 'unsupported'

await notify.requestPermission(); // 'granted' | 'denied' | 'default'

Always call from a user gesture

Chrome requires a user activation for Notification.requestPermission(); Firefox shows a quieter prompt; Safari only allows pushes for sites added to the Home Screen. Calling this on page load produces dismissals and permanently denied permissions. subscribe() calls it for you — wrap the whole call in a click handler.

subscribe()

try {
  const { subscription, created } = await notify.subscribe();
  console.log(created ? 'New subscriber' : 'Subscription refreshed', subscription.id);
} catch (error) {
  console.error(error.code, error.message);
}

Resolution order inside subscribe():

  1. Feature detection and a secure-context check.
  2. Notification.requestPermission().
  3. Fetch the VAPID public key (unless supplied in the constructor).
  4. Register the Service Worker and wait until it is active.
  5. Reuse an existing PushSubscription or create one with userVisibleOnly: true.
  6. POST /v1/subscriptions with { website_id, subscription: { endpoint, keys, expiration_time } }.

Re-subscribing with the same endpoint is safe: the server updates the stored key material and reactivates the row.

unsubscribe()

const { removed } = await notify.unsubscribe();

Makes a DELETE /v1/subscriptions call for the current endpoint, then calls pushSubscription.unsubscribe(). If the API call fails the browser subscription is still removed and the error is passed to onError. Removal is idempotent: unsubscribing twice is not an error.

getSubscriptionStatus()

const status = await notify.getSubscriptionStatus();
// { supported: true, permission: 'granted', subscribed: true, endpoint: 'https://fcm.googleapis.com/...' }

This reflects browser state only. It does not ask the API whether the endpoint is still valid — push services can invalidate an endpoint at any time. Use GET /v1/subscriptions/count on your server for authoritative numbers.

Error handling

Every rejection is a CreatorNotifyError with a stable code:

CodeMeaningWhat to do
MISSING_WEBSITE_IDConstructor called without websiteIdPass the public identifier
UNSUPPORTEDNo Service Worker / Push API, or insecure contextHide the button; explain HTTPS
PERMISSION_DENIEDThe user dismissed or blocked the promptShow instructions for browser settings
PERMISSION_ERRORThe browser threw while promptingRetry on a later gesture
SERVICE_WORKER_ERRORRegistration failed — wrong path, wrong origin, no HTTPSCheck that the file is served from your origin with text/javascript
SITE_NOT_FOUNDUnknown websiteId, or the Origin does not match the registrationRe-check the identifier and the registered origin
SITE_EXPIREDThe registration lapsed (no publish for seven days)Reactivate server-side; subscribers keep working afterwards
INVALID_SUBSCRIPTIONMalformed endpoint or keysRe-subscribe; if it persists the payload is corrupt
PUSH_ENDPOINT_NOT_ALLOWEDEndpoint host not in ALLOWED_PUSH_HOSTSAdd the push service host to the allow-list
RATE_LIMIT_EXCEEDEDToo many subscription calls from one clientBack off; the limit resets within a minute
NETWORK_ERRORThe API was unreachable or CORS blocked the requestCheck apiBaseUrl and the CORS section of Troubleshooting
SUBSCRIPTION_CHANGEDThe browser rotated the subscription (delivered via onError)Call subscribe() again on the next interaction

Service Worker messages

notify.onError = (error) => {
  if (error.code === 'SUBSCRIPTION_CHANGED') showRenewBanner();
};

When the browser fires pushsubscriptionchange, the Service Worker tells the page. The SDK surfaces it as a SUBSCRIPTION_CHANGED error; re-subscribing from a gesture restores delivery.

Complete example

<button id="notify-toggle" type="button" hidden>Enable notifications</button>
<p id="notify-status" role="status"></p>

<script src="/creator-notify.js"></script>
<script>
  const notify = new CreatorNotify({
    websiteId: 'pub_8d1f0c...',
    apiBaseUrl: 'https://YOUR-API-HOST'
  }).init();

  const button = document.getElementById('notify-toggle');
  const status = document.getElementById('notify-status');

  if (!notify.isSupported()) {
    status.textContent = 'This browser cannot show push notifications.';
  } else {
    notify.getSubscriptionStatus().then((state) => {
      button.hidden = false;
      button.textContent = state.subscribed ? 'Disable notifications' : 'Enable notifications';
    });

    button.addEventListener('click', async () => {
      button.disabled = true;
      try {
        const state = await notify.getSubscriptionStatus();
        if (state.subscribed) {
          await notify.unsubscribe();
          button.textContent = 'Enable notifications';
          status.textContent = 'Notifications disabled.';
        } else {
          await notify.subscribe();
          button.textContent = 'Disable notifications';
          status.textContent = 'Subscribed.';
        }
      } catch (error) {
        status.textContent = `${error.code}: ${error.message}`;
      } finally {
        button.disabled = false;
      }
    });
  }
</script>