> ## Documentation Index
> Fetch the complete documentation index at: https://notifizz.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The Notifizz Subscribe Widget

> Embed the subscribe button and avatar stack in your app — the React package, the Vanilla package, the script tag, and how appearance is configured.

# Subscribe widget

A button and a stack of avatars, mounted next to whatever your users can follow. Clicking it records a [subscription](/docs/concepts/subscriptions); clicking it again removes one. Mount as many as the page has resources — instances watching the same resource share their state, so subscribing on a card updates the detail panel without a refetch.

This page is for developers. The backend half — minting the token that authorises a mount — is [subscriptions from your backend](/docs/sdks/subscriptions/backend).

<Warning>
  **Closed beta.** Embedding the subscribe widget in your own app is enabled per organisation and is off by default. The packages below are published, but the **Subscribe widget** screen in your dashboard shows a preview instead of a configuration form until the feature is switched on. Ask your Notifizz contact to enable it for your project.
</Warning>

## TL;DR

* **React** — `@notifizz/subscribe-react`. A provider, a drop-in component, and a hook for custom UI. No script tag, nothing injected into the page.
* **Everything else** — `@notifizz/subscribe-vanilla`. Loads the widget script for you and returns a handle per mount.
* Both need three things at the top: the **front API key**, the **mode** (`secure` or `public`), and the **subscriber id** of the signed-in user.
* Each mount needs a **`resourceId`**, plus a **`hash`** when the environment is in `secure` mode — [computed by your backend](/docs/sdks/subscriptions/backend#generatesubscribetoken).
* N components on one resource make **one network request**, not N, and stay in sync with each other.
* Appearance — colour, labels, avatar count — is set once in the dashboard, not per mount.

## The three packages

| Package                       | Version | What it is                                                                                                                      | Install                                   |
| ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| `@notifizz/subscribe-react`   | 2.0.1   | React components and hook. Talks to Notifizz directly — no widget script involved.                                              | `npm install @notifizz/subscribe-react`   |
| `@notifizz/subscribe-vanilla` | 2.0.1   | Framework-agnostic wrapper. Injects the widget script and exposes an imperative API.                                            | `npm install @notifizz/subscribe-vanilla` |
| `@notifizz/subscribe`         | 1.0.0   | The widget bundle itself, plus its public TypeScript types. Normally loaded from the Notifizz widget host by the wrapper above. | `npm install @notifizz/subscribe`         |

Pick the wrapper for your stack. `@notifizz/subscribe` is worth installing only when you drive the widget through the script tag and want its types in your editor.

Both wrappers follow the [SDK versioning policy](/docs/sdks/versioning-policy) — pin to a major.

## React

### Provide the identity once

`<NotifizzProvider>` holds what does not change between mounts. Place it once, above everything that renders a subscribe button, **after** the user is authenticated:

```tsx theme={null}
import { NotifizzProvider } from '@notifizz/subscribe-react';

<NotifizzProvider
  apiKey={import.meta.env.VITE_NOTIFIZZ_FRONT_API_KEY}
  mode="secure"
  subscriberId={user.id}
>
  <App />
</NotifizzProvider>
```

| Prop           | Required | Default                       | Notes                                                                                               |
| -------------- | -------- | ----------------------------- | --------------------------------------------------------------------------------------------------- |
| `apiKey`       | yes      | —                             | The environment's **Front API Key**. Public by design; see [API keys](/docs/environments/api-keys). |
| `mode`         | yes      | —                             | `'secure'` or `'public'`. Must match the environment's setting.                                     |
| `subscriberId` | yes      | —                             | Who the current user is. Byte-identical to what your backend hashes.                                |
| `apiUrl`       | no       | `https://api.notifizz.com/v1` | Region or staging endpoint.                                                                         |
| `maxAvatars`   | no       | `5`                           | Fallback used until the dashboard appearance settings arrive.                                       |

Rendering a subscribe component outside the provider throws immediately rather than failing silently later.

### Drop in the button

```tsx theme={null}
import { NotifizzSubscribe } from '@notifizz/subscribe-react';

<NotifizzSubscribe resourceId="proj_4a19f" hash={project.subscribeHash} />
```

| Prop                                  | Required         | Notes                                                            |
| ------------------------------------- | ---------------- | ---------------------------------------------------------------- |
| `resourceId`                          | yes              | The thing being followed.                                        |
| `hash`                                | in `secure` mode | The hash for this exact `(subscriberId, resourceId)` pair.       |
| `renderSubscriber`                    | no               | Custom renderer per visible avatar: `(subscriber) => ReactNode`. |
| `subscribeLabel` / `unsubscribeLabel` | no               | Override the labels set in the dashboard.                        |
| `maxAvatars`                          | no               | Override the avatar count set in the dashboard.                  |
| `className` / `style`                 | no               | Applied to the outer wrapper.                                    |

Missing a required prop throws at render — into the nearest error boundary — rather than producing a `401` you have to find in the network tab.

### One per resource, any number per page

```tsx theme={null}
function Board({ projects }: { projects: Project[] }) {
  return (
    <div className="board">
      {projects.map((p) => (
        <Card key={p.id}>
          <h3>{p.name}</h3>
          <NotifizzSubscribe resourceId={p.id} hash={p.subscribeHash} />
        </Card>
      ))}
    </div>
  );
}
```

Two components pointing at the same `resourceId` — a card and the detail panel it opens — share one store. Clicking subscribe in one updates the other with no refetch, and mounting both in the same tick fires a single request for the subscriber list.

### Custom UI with `useSubscription`

When the default button does not fit, take the state and render your own:

```tsx theme={null}
import { useSubscription } from '@notifizz/subscribe-react';

function FollowStar({ resourceId, hash }: { resourceId: string; hash: string }) {
  const { isSubscribed, subscriberCount, isLoading, toggle } = useSubscription(resourceId, hash);

  return (
    <button onClick={toggle} disabled={isLoading} aria-pressed={isSubscribed}>
      {isSubscribed ? 'Following' : 'Follow'} · {subscriberCount}
    </button>
  );
}
```

| Returns                                      | Type                  | Notes                                                             |
| -------------------------------------------- | --------------------- | ----------------------------------------------------------------- |
| `isSubscribed`                               | `boolean`             | True when the current subscriber id is in the list.               |
| `subscribers`                                | `SubscriberInfo[]`    | `{ id, name?, email?, avatarUrl? }`. No `email` in `public` mode. |
| `subscriberCount`                            | `number`              |                                                                   |
| `isLoading`                                  | `boolean`             | Shared per resource — true while a refresh is in flight.          |
| `subscribe()` / `unsubscribe()` / `toggle()` | `() => Promise<void>` |                                                                   |
| `refresh()`                                  | `() => Promise<void>` | Force a refetch.                                                  |

`useSubscription` powers `<NotifizzSubscribe>`, so mixing the two on one resource is fine — they read the same store.

### Naming the avatars

By default an avatar is the **first character of whatever the subscription holds**, coloured deterministically from the id. Since a subscription created by the widget stores only the subscriber id, that usually means the first character of the id — legible, but not a name.

That is on purpose: Notifizz is not the place your user directory lives. Render the names from wherever they already are:

```tsx theme={null}
<NotifizzSubscribe
  resourceId={project.id}
  hash={project.subscribeHash}
  renderSubscriber={(sub) => {
    const member = members.find((m) => m.id === sub.id);
    return (
      <img
        src={member?.avatarUrl}
        alt={member?.fullName ?? sub.id}
        title={member?.fullName ?? sub.id}
        style={{ width: 28, height: 28, borderRadius: '50%' }}
      />
    );
  }}
/>
```

### Lifecycle

React handles it. Unmounting a component detaches its listener; there is no `destroy()` to call. Changing `resourceId` re-points the component at the new resource, and the previous resource's state stays in the shared store for whatever else is watching it.

Unmounting a component does **not** unsubscribe the person. Only pressing the button does.

## Vanilla JS and TypeScript

For Angular, Vue, Svelte, server-rendered pages, or anything else. The wrapper injects the widget script the first time you call it and returns a handle per mount.

```ts theme={null}
import { createSubscribe } from '@notifizz/subscribe-vanilla';

const subscribe = createSubscribe({
  apiKey: 'YOUR_FRONT_API_KEY',
  mode: 'secure',
  subscriberId: 'user_8f21',
});

const widget = await subscribe.mount({
  container: '#card-proj_4a19f',
  resourceId: 'proj_4a19f',
  hash: 'c4f0…',
});

widget.onStateChange((s) => console.log(s.isSubscribed, s.subscriberCount));
```

### `createSubscribe(options)`

| Option           | Required | Default                       | Notes                                                                                   |
| ---------------- | -------- | ----------------------------- | --------------------------------------------------------------------------------------- |
| `apiKey`         | yes      | —                             | The environment's Front API Key.                                                        |
| `mode`           | yes      | —                             | `'secure'` or `'public'`.                                                               |
| `subscriberId`   | yes      | —                             | Who the current user is.                                                                |
| `apiUrl`         | no       | `https://api.notifizz.com/v1` | Region or staging endpoint.                                                             |
| `serverUrl`      | no       | `https://widget.notifizz.com` | Where the widget script is loaded from.                                                 |
| `widgetPath`     | no       | `/v1/subscribe-loader.js`     | Path of the loader on `serverUrl`.                                                      |
| `autoMount`      | no       | `false`                       | Scan the DOM for `[data-notifizz-subscribe]`. Off here — you are mounting imperatively. |
| `maxAvatars`     | no       | `5`                           | Fallback until the dashboard appearance settings arrive.                                |
| `readyTimeoutMs` | no       | `10000`                       | How long `mount()` waits for the script before rejecting.                               |

Calling `createSubscribe()` more than once is safe — the script is injected once and shared.

### `mount(options)`

| Option       | Required         | Notes                                                |
| ------------ | ---------------- | ---------------------------------------------------- |
| `container`  | yes              | CSS selector or `HTMLElement`.                       |
| `resourceId` | yes              | The thing being followed.                            |
| `hash`       | in `secure` mode | The hash for this `(subscriberId, resourceId)` pair. |

Returns a handle:

| Member                                       | Signature                                              | Notes                                                                                                 |
| -------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `resourceId`                                 | `string`                                               | Read-only.                                                                                            |
| `subscribe()` / `unsubscribe()` / `toggle()` | `() => Promise<void>`                                  |                                                                                                       |
| `refresh()`                                  | `() => Promise<void>`                                  | Force a refetch of the subscriber list.                                                               |
| `getState()`                                 | `() => { isSubscribed, subscriberCount, subscribers }` | Snapshot.                                                                                             |
| `onStateChange(cb)`                          | `(cb) => () => void`                                   | Fires on this resource's changes, including those made by other instances. Returns a detach function. |
| `destroy()`                                  | `() => void`                                           | Tears down the DOM and the listeners for this instance.                                               |

`subscribe.destroy()` tears down every instance created by that `subscribe` object. It deliberately leaves the script in place — a later mount reuses it.

`mount()` rejects when the script fails to load within `readyTimeoutMs` (network or CSP), when the container is missing or already has a widget, or when `secure` mode is on and no hash was supplied.

### Lifecycle in a component framework

Pair `mount()` with your framework's teardown hook, and guard against the component disappearing while the promise is in flight:

```ts theme={null}
let widget: SubscribeHandle | null = null;
let cancelled = false;

subscribe
  .mount({ container: el, resourceId: project.id, hash: project.subscribeHash })
  .then((w) => (cancelled ? w.destroy() : (widget = w)));

// on teardown
cancelled = true;
widget?.destroy();
```

## Script tag and auto-mount

For server-rendered pages with no build step. Load the loader, mark the elements, initialise once:

```html theme={null}
<script src="https://widget.notifizz.com/v1/subscribe-loader.js"></script>

<div data-notifizz-subscribe="proj_4a19f" data-notifizz-hash="c4f0…"></div>
<div data-notifizz-subscribe="proj_77c02" data-notifizz-hash="9b31…"></div>

<script>
  notifizzSubscribe('init', {
    apiKey: 'YOUR_FRONT_API_KEY',
    mode: 'secure',
    subscriberId: 'user_8f21',
  });
</script>
```

The loader installs a queue immediately, so calls made before the widget finishes downloading are replayed in order — the `init` above does not need to wait for anything.

With `autoMount` left on (the default for this path), the widget scans for `[data-notifizz-subscribe]` and keeps watching: elements injected later, by a template fragment or an `innerHTML` update, are mounted as they appear.

### Driving it imperatively

The same global takes commands:

| Command       | Call                                                                                          | Returns                                     |
| ------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `init`        | `notifizzSubscribe('init', { apiKey, mode, subscriberId, autoMount?, apiUrl?, maxAvatars? })` | —                                           |
| `mount`       | `notifizzSubscribe('mount', { container, resourceId, hash? })`                                | A handle, same shape as the Vanilla package |
| `destroy`     | `notifizzSubscribe('destroy', handleOrContainerOrSelector)`                                   | —                                           |
| `getInstance` | `notifizzSubscribe('getInstance', containerOrSelector)`                                       | The handle mounted there, or `null`         |

Per-instance operations live on the handle, never on the global — a page with twenty widgets has no meaningful notion of *the current one*.

Two ways to know the API is live, if you need to call `mount` immediately rather than queue it: the `notifizz-subscribe:ready` event on `window`, or the synchronous flag `window._notifizzSubscribe._ready`.

### Content Security Policy

The Vanilla and script-tag paths load a script from `https://widget.notifizz.com` and call `https://api.notifizz.com`. Allow both:

```
Content-Security-Policy: script-src https://widget.notifizz.com 'self'; connect-src https://api.notifizz.com 'self';
```

The React package loads no script — it only needs `connect-src`.

## Appearance

Set once for the whole organisation, in the dashboard under **Settings → Subscribers & privacy → Subscribe widget**, with a live preview. Every mounted widget fetches it once per page load and caches it, so N instances cost one request.

| Setting                        | Effect                                                                                      |
| ------------------------------ | ------------------------------------------------------------------------------------------- |
| Primary colour                 | Button colour. Inherits your global brand colour unless overridden here.                    |
| Border style                   | Roundness of the button and avatars. Inherits the global brand roundness unless overridden. |
| Subscribe / Unsubscribe labels | The two button labels.                                                                      |
| Max visible avatars            | How many faces show before the overflow count.                                              |
| Avatars position               | Avatars left or right of the button.                                                        |

Two notes on what applies where. **Avatar position and border style are applied by the loader-based widget** — the Vanilla and script-tag paths; the React package renders avatars on the left with rounded controls. And per-mount props always win: `subscribeLabel`, `unsubscribeLabel` and `maxAvatars` on `<NotifizzSubscribe>` override the dashboard for that instance.

Because appearance is cached per page load, a change made in the dashboard shows up on the next reload, not in an open tab.

## Failure modes

| Symptom                               | Cause                                                                                                                                           | Fix                                                                                                                 |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Nothing renders, no request           | Provider or `init` missing, or `apiKey` / `subscriberId` empty                                                                                  | The React package throws on this; the loader logs a mount error to the console                                      |
| Mount throws about a missing `hash`   | Environment is in `secure` mode                                                                                                                 | Pass the hash — see [generateSubscribeToken](/docs/sdks/subscriptions/backend#generatesubscribetoken)               |
| Requests return `401`                 | Wrong front API key, or a hash that does not match this exact pair                                                                              | Compare the subscriber id and resource id hashed server-side with the ones the widget uses, character for character |
| Button appears to do nothing          | Subscribe and unsubscribe are best-effort in the browser: a failed request never throws, and the list is reconciled by the refresh that follows | Look at the response codes for the two calls                                                                        |
| Avatars show letters instead of faces | The subscription carries no display data — expected                                                                                             | Use `renderSubscriber`, or render your own UI with `useSubscription`                                                |
| No `email` on subscribers             | Environment is in `public` mode, which never returns addresses                                                                                  | Use `secure` mode if you need them                                                                                  |
| `429` on the widget endpoints         | Per-IP rate limit on the browser-facing routes                                                                                                  | Expected when hammering from one address; not a limit real users reach                                              |
| Container already has a widget        | Two mounts on the same element                                                                                                                  | Call `destroy()` before remounting                                                                                  |

## FAQ

<AccordionGroup>
  <Accordion title="Can I use the widget without a signed-in user?">
    Only in `public` mode, and only for resources with no privacy or authorisation stake — anonymous subscribers still need a stable `subscriberId`, and in `public` mode any browser can claim any id. Anything behind a login belongs in `secure` mode.
  </Accordion>

  <Accordion title="Does unmounting the widget unsubscribe the person?">
    No. Unmounting removes UI. The subscription is a durable record and only the button removes it.
  </Accordion>

  <Accordion title="How many widgets can one page carry?">
    As many as it has resources. Mounts on the same resource share one request and one store, and appearance is fetched once for the page — the cost of the twentieth widget is a DOM node.
  </Accordion>

  <Accordion title="Can I read the subscriber list without rendering anything?">
    In React, yes — `useSubscription(resourceId, hash)` gives you the list with no markup attached. In the Vanilla package the list arrives through a mounted handle, so mount into a hidden container if you only want the data.
  </Accordion>

  <Accordion title="Is there an Angular package?">
    Not today. Use `@notifizz/subscribe-vanilla` from a directive or a component, pairing `mount()` with `ngOnDestroy`.
  </Accordion>

  <Accordion title="Which package should I install if I already use the notification center?">
    The subscribe packages are independent of the notification-center ones — different scripts, different endpoints, different purpose. Installing both is normal, and neither interferes with the other.
  </Accordion>

  <Accordion title="The avatar stack is empty even though I just subscribed.">
    Subscribe and unsubscribe do not throw in the browser; they rely on the refresh that follows to reconcile with the server. An empty stack after a click almost always means the write was refused — check the response status of the subscribe call, then the hash.
  </Accordion>
</AccordionGroup>

## See also

<CardGroup cols={2}>
  <Card title="Subscriptions from your backend" icon="server" href="/docs/sdks/subscriptions/backend">
    Minting the hash, and notifying a group.
  </Card>

  <Card title="Subscriptions" icon="user-plus" href="/docs/concepts/subscriptions">
    What a subscription is and how it becomes a notification.
  </Card>

  <Card title="API keys" icon="key" href="/docs/environments/api-keys">
    Where the Front API Key comes from.
  </Card>

  <Card title="Versioning policy" icon="tag" href="/docs/sdks/versioning-policy">
    How these packages are versioned and released.
  </Card>
</CardGroup>
