Documentation
1. Mint a token in your backend
Your app has a signing key. Your backend already knows who is logged in, so it signs a short-lived token naming that user. The key never reaches a browser.
import { createHmac } from 'node:crypto'
function mintToken(appId, userId, signingKey, ttlSeconds = 900) {
const payload = {
aid: appId,
sub: userId, // your own user id
exp: Math.floor(Date.now() / 1000) + ttlSeconds,
}
const encoded = Buffer.from(JSON.stringify(payload)).toString('base64url')
const signature = createHmac('sha256', signingKey).update(encoded).digest('base64url')
return `${encoded}.${signature}`
}2. Put the bell on the page
<script src="http://localhost:3105/embed/notify-bell.js" defer></script>
<notify-bell app-id="app_…" token="…" api-url="http://localhost:3105"></notify-bell>It is a custom element, so it works the same in React, Vue, Svelte, Rails or a plain page. Set the token attribute again when you refresh it and the element reconnects on its own.
3. Publish from your backend
curl -X POST http://localhost:3105/api/v1/apps/APP_ID/notifications \
-H "Authorization: Bearer nb_…" \
-H "Content-Type: application/json" \
-d '{
"subscriberId": "usr_99812",
"title": "Deployment completed",
"body": "Your production build v1.4.2 was deployed.",
"category": "system",
"actionUrl": "https://app.example.com/deployments/123",
"dedupeKey": "deploy-123"
}'The subscriber is created on first publish, so there is no user-sync step. Pass a dedupeKey and a retry from your job runner will not ring the bell twice.
Styling it
The element renders into a shadow root, so your CSS cannot accidentally restyle it and its styles cannot leak into your page. Three parts are exposed deliberately:
notify-bell::part(bell) { color: #64748b; }
notify-bell::part(badge) { background: #2563eb; }
notify-bell::part(panel) { border-radius: 4px; }Honest limits
- Real-time fan-out is in-process. NotifyBell runs as a single instance; push is immediate. Nothing is ever lost either way, because the inbox is stored rather than replayed — but horizontal scaling needs a shared bus, and that is not built yet.
- There is no email, SMS or push. Deliberately. If you need multi-channel routing, this is not the product.
- A stream is held for fifteen minutes, then closed so the browser reconnects. That is normal and invisible; it keeps a wedged connection from looking healthy.