Subscriptions from your backend
Your backend has two jobs in the subscription flow, and both are small. It authorises the widget — one hash per person, per resource — and it notifies a group when something happens to the thing they follow. Everything else happens in the browser, in your own interface. This page is for developers. The concept it implements is described in subscriptions; the browser half is the subscribe widget.TL;DR
- Two widget authentication modes, chosen per environment:
secure(the default) requires a server-computed hash for every subscriber-and-resource pair,publictrusts the front API key alone. generateSubscribeToken(subscriberId, resourceId)mints that hash. It is a local HMAC computation — no network call, no rate limit, safe to compute per render.- The hash binds one person to one resource. It is not a session, it does not expire, and it authorises nothing else.
notifySubscribers(resourceId, properties)is a thin wrapper overtrack(): it emits the eventsubscription-enteredwithresourceIdmerged into the properties.- Any event works.
notifySubscribers()is a shortcut, not a requirement — your own event carrying the group id does the same job. - In
publicmode the subscriber list is returned without email addresses. Onlysecuremode may see them.
The two authentication modes
The widget talks to Notifizz from the browser, so it can only carry secrets that are safe to ship in a bundle. The mode decides what proof it must carry on top of the front API key. It is configured per environment, so your dev environment can be permissive while production is not.Why secure is the default
The front API key is public by design — it is embedded in your frontend bundle and visible to anyone who opens the network tab. In public mode it is the only credential, so the browser is free to name any subscriber id it likes: subscribe somebody else to a resource, unsubscribe them from one, or read who else is subscribed.
In secure mode the request must also carry a hash your backend computed with a secret the browser never sees. Notifizz recomputes it and compares the two in constant time; a mismatch is refused before anything is read or written. And because the hash proves the subscriber id, the server uses the proven id and ignores the one in the request body — a tampered body changes nothing.
That last property is why public mode also refuses to return email addresses. If any browser can pose as any subscriber, the subscriber list must not be a way to harvest addresses.
generateSubscribeToken
Mints the hash for one subscriber and one resource:
Getting the hash to the browser
Whatever you already do for authenticated data works. Two shapes cover almost everything:- Server-rendered page — compute the hash alongside the resource and pass it into the template. The widget receives it as a prop or a
data-attribute. - Single-page app — return the hash on the object your frontend already fetches. A list of projects becomes a list of
{ id, name, subscribeHash }.
Node.js — hash alongside the resource
- One hash per pair. A hash minted for
proj_4a19fis refused onproj_77c02. A page showing twenty cards needs twenty hashes. - It never expires. It is a binding, not a session. Revoking someone’s access to a resource in your product does not invalidate a hash you already handed them — which is fine, because the hash only ever authorises following that one resource and reading who else follows it.
authSecretKeynever leaves your backend. It is the same secret behindgenerateHashedTokenfor notification-center auth; see API keys. Shipping it to the browser would makesecuremode exactly as strong aspublicmode.
The subscriber id you hash must be byte-identical to the one the widget is initialised with.
User_8f21 and user_8f21 produce different hashes, and the widget gets a 401 with no other clue. If subscribing silently does nothing, compare those two strings first.notifySubscribers
Emits the event that a subscriber campaign listens to:
resourceId into the properties and calls track('subscription-entered', …). Everything true of track() is true here — the same idempotency handling, the same schema modes, the same failure behaviour.
Two consequences of it being a plain
track():
- A campaign has to exist on the
subscription-enteredevent, with Subscribers as its recipient. Without one, the event is recorded and nothing is sent — the same as any event nobody listens to. - In
strictschema mode you must declare the event first.strictrefuses to push an undeclared event, andnotifySubscribers()does not exempt itself:
Node.js — required in strict mode
soft mode the track goes through and the SDK warns.
You do not have to use it
notifySubscribers() exists so the common case is one call. It is not a privileged path: the campaign resolves the group from whichever event property carries the group id, so your own domain event does the same job and reads better in your codebase.
Node.js — your own event, same result
project.deployment_finished with Subscribers as the recipient, and the orchestrator resolves projectId as the group. Prefer this when the event already exists for other reasons — one event, several campaigns, no duplicate.
The property may hold a single id or an array of ids. An event naming several groups notifies their union, with anyone in more than one of them counted once. Never put a list of people in the payload — see why.
Replays and backfills
PassoccurredAt when the business event happened at a moment other than the one you are calling from — a replay, an offline batch, a history import. It dates the event itself:
Node.js
What the backend SDKs do not do
There is no supported server-side path to create or remove a subscription. Subscriptions come from the subscribe widget, where the person subscribing is the person consenting and the click is the record. A backend that could subscribe people on their behalf would turn that record into an assertion nobody made. Reading who is subscribed is likewise a widget concern: every mounted instance already exposes the list for its resource, shared across the components watching it. If your product genuinely needs an audience you assemble yourself rather than one people opt into, that is a different mechanism — resolve it with an enricher, or launch a broadcast against a CRM segment.Failure modes
FAQ
Can I switch an environment from public to secure later?
Can I switch an environment from public to secure later?
Yes, and existing subscriptions are unaffected — the mode governs how requests are authorised, not what is stored. What breaks is any page still mounting the widget without a hash: it starts refusing to mount. Ship the hash first, flip the mode second.
Should I cache the hash?
Should I cache the hash?
There is nothing to cache. It is one HMAC over two short strings, with no network call — recomputing it per render costs less than looking it up. Do not store it anywhere durable either: a hash you persist is a hash you have to invalidate, for no benefit.
Is the hash sensitive? It ends up in my HTML.
Is the hash sensitive? It ends up in my HTML.
It is a capability, not a secret: whoever holds it can follow that one resource as that one subscriber, and read who else follows it. That is exactly what the person it was minted for is allowed to do. Mint it only for the people your own authorisation rules already allow on the resource, and never for a subscriber id other than the caller’s own.
Do I need the webhook signing secret for any of this?
Do I need the webhook signing secret for any of this?
No. The third constructor argument is only needed to expose enrichers. A client that only mints tokens and tracks events needs the auth secret and the SDK secret.
Can several events target the same group?
Can several events target the same group?
Yes, and that is the normal shape. A group is just an id; any number of campaigns, on any number of events, can resolve the same one. The subscription is a statement about the resource, not about a single kind of message.
What identifier should the subscriber id be?
What identifier should the subscriber id be?
The one your target channel needs. For the notification center it must match the identity your widget authenticates with, exactly. For email it should be resolvable to an address — either it is the address, or an enricher maps it. See reaching a subscriber on a channel.
See also
Subscribe widget
The browser half — React, Vanilla, and the script tag.
Subscriptions
What a subscription is and how it becomes a notification.
API keys
Which secret goes where, and which one signs this hash.
Event tracking
track(), schema modes and idempotency — all of which apply here.