# A notification bell in React

> Show in-app notifications from Nordva Launch in a React bell: poll the signed-in user''s inbox, show the unread count, mark read.

This builds a small bell on top of [In-app notifications](/guides/notifications/). Every plan includes notifications.

If you would rather not build one, [the bell widget](/guides/notifications/#the-bell-widget) is a single script tag with the same behaviour. Build your own when the bell has to be part of your React tree.

## Prerequisites

- A secret key (`nv_live_...`) on your server, to send notifications.
- A publishable key (`nv_pub_...`) in your frontend, to read them.
- The project's signing secret on your server, to compute each user's `user_hash`. See [Identity verification](/guides/notifications-identity/).

## Step 1: Send a notification from your server

```typescript
// Server-side: Next.js route handler, server action, or any Node/Edge function
async function notifyUser(userId: string, title: string, body: string) {
  const res = await fetch('https://api.nordva.dev/v1/notifications', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.NORDVA_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ user_id: userId, title, body, action_url: '/dashboard', icon: 'check' }),
  });
  // 201: sent. 403 PLAN_LIMIT_REACHED: this month's notifications are used up.
}

await notifyUser('user_abc123', 'Export ready', 'Your CSV export has been generated.');
```

To post to everyone instead, send `audience: "all"` and no `user_id`.

## Step 2: Give the page the user id and its hash

```typescript
// Server-side, where you know who is signed in

const userHash = createHmac('sha256', process.env.NORDVA_NOTIFICATIONS_SECRET!).update(user.id).digest('hex');
// Pass user.id and userHash to the page as props. Never the secret.
```

## Step 3: The bell

```tsx
'use client';

interface Notification {
  id: string;
  title: string;
  body: string;
  action_url: string | null;
  icon: string;
  created_at: string;
  read: boolean;
  broadcast: boolean;
}

const NORDVA_KEY = process.env.NEXT_PUBLIC_NORDVA_KEY!;
const API = 'https://api.nordva.dev/v1/notifications';

  const [notifications, setNotifications] = useState<Notification[]>([]);
  const [unread, setUnread] = useState(0);
  const [pollSeconds, setPollSeconds] = useState(60);
  const [open, setOpen] = useState(false);
  const headers = { 'Authorization': `Bearer ${NORDVA_KEY}`, 'X-Nordva-User-Hash': userHash };

  const poll = useCallback(async () => {
    const res = await fetch(`${API}/inbox?user_id=${encodeURIComponent(userId)}`, { headers });
    if (!res.ok) return;
    const { data } = await res.json();
    setNotifications(data.notifications);
    setUnread(data.unread_count);
    setPollSeconds(data.poll_interval_seconds);
  }, [userId, userHash]);

  useEffect(() => {
    void poll();
    const interval = setInterval(poll, pollSeconds * 1000);
    return () => clearInterval(interval);
  }, [poll, pollSeconds]);

  async function markRead(n: Notification) {
    if (!n.read) {
      await fetch(`${API}/${n.id}/read`, {
        method: 'PATCH',
        headers: { ...headers, 'Content-Type': 'application/json' },
        body: JSON.stringify({ user_id: userId }),
      });
      setNotifications(prev => prev.map(x => (x.id === n.id ? { ...x, read: true } : x)));
      setUnread(u => Math.max(0, u - 1));
    }
    if (n.action_url) window.location.href = n.action_url;
  }

  return (
    <div className="bell">
      <button onClick={() => setOpen(o => !o)} aria-label={`Notifications, ${unread} unread`}>
        🔔 {unread > 0 && <span className="bell-count">{unread}</span>}
      </button>
      {open && (
        <div className="bell-panel">
          {notifications.length === 0 ? (
            <p>No notifications</p>
          ) : (
            notifications.map(n => (
              <button key={n.id} className={n.read ? 'bell-item' : 'bell-item unread'} onClick={() => void markRead(n)}>
                <strong>{n.title}</strong>
                <span>{n.body}</span>
              </button>
            ))
          )}
        </div>
      )}
    </div>
  );
}
```

Style the classes in your own stylesheet: inline `style` attributes are blocked by a strict Content Security Policy.

`PATCH /v1/notifications/read-all` with `{ "user_id": "..." }` and the same header marks everything read, for a "Mark all read" button.

## Common errors

| Error | Cause | Fix |
|---|---|---|
| `403 USER_HASH_INVALID` | No `user_hash`, a wrong one, or no signing secret yet | Compute the hash on your server with the project's signing secret. See [Identity verification](/guides/notifications-identity/). |
| `403 PLAN_LIMIT_REACHED` | This month's notifications are used up | Wait for the 1st (UTC) or upgrade at [launch.nordva.dev/pricing](https://launch.nordva.dev/pricing). |
| `429 RATE_LIMITED` | The bell asked more often than the plan's hourly budget per user | Poll at `poll_interval_seconds`. |
| `401 INVALID_API_KEY` | Wrong key | Secret key to send, publishable key in the browser. |
| `403 KEY_INSUFFICIENT_PERMISSIONS` | A publishable key was used to send | Send from your server with the secret key. |

Source: https://docs.nordva.dev/guides/notifications-polling/
