Skip to content

In-App Notifications

  1. Your server calls POST /v1/notifications with a secret key when something happens (job complete, new message, etc.)
  2. Your frontend polls GET /v1/notifications/unread with a publishable key every 30 seconds
  3. The user sees a bell icon with an unread count. Clicking marks notifications as read via PATCH /v1/notifications/:id/read
  • Nordva Launch account on Builder plan
  • Secret key (nv_live_...) for your server
  • Publishable key (nv_pub_...) for your frontend

Step 1: Send a notification from your server

Section titled “Step 1: Send a notification from your server”
// Server-side: Next.js API route, 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: 'https://app.yoursite.com/dashboard',
icon: 'check',
}),
});
// 201: queued, 403: not on Builder plan
}
// Example: call after a background export completes
await notifyUser('user_abc123', 'Export ready', 'Your CSV export has been generated.');

Body fields:

FieldRequiredDescription
user_idyesYour app’s user identifier (any string)
titleyesMax 120 chars
bodyyesMax 500 chars. Plain text only.
action_urlnoDeep link into your app
iconnocheck, warning, info, or error

Step 2: Poll for unread notifications in React

Section titled “Step 2: Poll for unread notifications in React”
'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;
}
const NORDVA_KEY = process.env.NEXT_PUBLIC_NORDVA_KEY!;
const POLL_INTERVAL = 30_000; // 30 seconds — match meta.poll_interval_seconds
export function NotificationBell({ userId }: { userId: string }) {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [open, setOpen] = useState(false);
const poll = useCallback(async () => {
const res = await fetch(
`https://api.nordva.dev/v1/notifications/unread?user_id=${encodeURIComponent(userId)}`,
{ headers: { 'Authorization': `Bearer ${NORDVA_KEY}` } }
);
if (!res.ok) return;
const { data } = await res.json();
// data.notifications is the array; data.poll_interval_seconds is the suggested cadence.
setNotifications(data.notifications);
}, [userId]);
useEffect(() => {
void poll();
const interval = setInterval(poll, POLL_INTERVAL);
return () => clearInterval(interval);
}, [poll]);
async function markRead(id: string) {
await fetch(`https://api.nordva.dev/v1/notifications/${id}/read`, {
method: 'PATCH',
headers: { 'Authorization': `Bearer ${NORDVA_KEY}` },
});
setNotifications(prev => prev.filter(n => n.id !== id));
}
return (
<div style={{ position: 'relative' }}>
<button onClick={() => setOpen(o => !o)} style={{ position: 'relative' }}>
🔔
{notifications.length > 0 && (
<span style={{ position: 'absolute', top: -4, right: -4, background: '#ef4444', color: '#fff', borderRadius: '50%', fontSize: 10, width: 16, height: 16, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{notifications.length}
</span>
)}
</button>
{open && (
<div style={{ position: 'absolute', right: 0, top: '100%', width: 320, background: '#fff', borderRadius: 12, boxShadow: '0 8px 32px rgba(0,0,0,0.15)', zIndex: 1000, overflow: 'hidden' }}>
{notifications.length === 0 ? (
<p style={{ padding: '16px', color: '#6b7280', fontSize: 14 }}>No new notifications</p>
) : (
notifications.map(n => (
<div
key={n.id}
onClick={() => { void markRead(n.id); if (n.action_url) window.location.href = n.action_url; }}
style={{ padding: '12px 16px', borderBottom: '1px solid #f3f4f6', cursor: 'pointer' }}
>
<p style={{ margin: 0, fontSize: 13, fontWeight: 600 }}>{n.title}</p>
<p style={{ margin: '4px 0 0', fontSize: 12, color: '#6b7280' }}>{n.body}</p>
</div>
))
)}
</div>
)}
</div>
);
}

Add to your app header:

app/layout.tsx
import { NotificationBell } from '@/components/NotificationBell';
export default function RootLayout({ children, userId }: { children: React.ReactNode; userId: string }) {
return (
<html>
<body>
<header>
<NotificationBell userId={userId} />
</header>
{children}
</body>
</html>
);
}
ErrorCauseFix
403 PLAN_REQUIREDNot on Builder planUpgrade at launch.nordva.dev/pricing
401 INVALID_API_KEYWrong keyUse secret key for POST, publishable key for GET/PATCH
403 KEY_INSUFFICIENT_PERMISSIONSSecret key used in browserUse nv_pub_... key in frontend polling code