A notification bell in React
This builds a small bell on top of In-app notifications. Every plan includes notifications.
If you would rather not build one, 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
Section titled “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.
Step 1: Send a notification from your server
Section titled “Step 1: Send a notification from your server”// Server-side: Next.js route handler, server action, or any Node/Edge functionasync 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
Section titled “Step 2: Give the page the user id and its hash”// Server-side, where you know who is signed inimport { createHmac } from 'node:crypto';
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
Section titled “Step 3: The bell”'use client';
import { useEffect, useState, useCallback } from 'react';
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';
export function NotificationBell({ userId, userHash }: { userId: string; userHash: string }) { 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
Section titled “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. |
403 PLAN_LIMIT_REACHED | This month’s notifications are used up | Wait for the 1st (UTC) or upgrade at 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. |