In-App Notifications
How it works
Section titled “How it works”- Your server calls
POST /v1/notificationswith a secret key when something happens (job complete, new message, etc.) - Your frontend polls
GET /v1/notifications/unreadwith a publishable key every 30 seconds - The user sees a bell icon with an unread count. Clicking marks notifications as read via
PATCH /v1/notifications/:id/read
Prerequisites
Section titled “Prerequisites”- 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 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: 'https://app.yoursite.com/dashboard', icon: 'check', }), }); // 201: queued, 403: not on Builder plan}
// Example: call after a background export completesawait notifyUser('user_abc123', 'Export ready', 'Your CSV export has been generated.');Body fields:
| Field | Required | Description |
|---|---|---|
user_id | yes | Your app’s user identifier (any string) |
title | yes | Max 120 chars |
body | yes | Max 500 chars. Plain text only. |
action_url | no | Deep link into your app |
icon | no | check, 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:
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> );}Common errors
Section titled “Common errors”| Error | Cause | Fix |
|---|---|---|
403 PLAN_REQUIRED | Not on Builder plan | Upgrade at launch.nordva.dev/pricing |
401 INVALID_API_KEY | Wrong key | Use secret key for POST, publishable key for GET/PATCH |
403 KEY_INSUFFICIENT_PERMISSIONS | Secret key used in browser | Use nv_pub_... key in frontend polling code |