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