skills/inbox-integration/SKILL.md
Integrate Novu's in-app notification inbox into web applications. Supports React, Next.js, and vanilla JavaScript. Includes the Inbox component (bell icon + notification feed), composable components (Bell, Notifications, InboxContent, Preferences), headless hooks, branded theming, custom render props, multi-tenancy via contexts, tabs, localization, and HMAC security. Use when adding an in-app notification center, bell icon, notification feed, real-time notification updates, or building a personalized and branded notification experience.
npx skillsauth add novuhq/skills novu-inbox-integrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Add an in-app notification center to your web application. The Inbox component provides a bell icon, notification feed, read/archive management, action buttons, and real-time WebSocket updates — all theme-able and personalizable to match your product.
| Package | Use For |
| --- | --- |
| @novu/react | React 18/19 applications |
| @novu/nextjs | Next.js (App Router + Pages Router) |
| @novu/js | Vanilla JavaScript / non-React frameworks |
npm install @novu/react
import { Inbox } from "@novu/react";
function App() {
return (
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="subscriber-123"
subscriberHash="HMAC_HASH" // Required if HMAC encryption is enabled
/>
);
}
This renders a bell icon with unread count. Clicking it opens a popover with the notification feed.
npm install @novu/nextjs
// components/NotificationInbox.tsx
"use client";
import { Inbox } from "@novu/nextjs";
export function NotificationInbox() {
return (
<Inbox
applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID!}
subscriberId="subscriber-123"
subscriberHash="HMAC_HASH"
/>
);
}
Important: The Inbox is a client component — use "use client" directive in Next.js App Router.
import { Inbox } from "@novu/nextjs";
export default function NotificationsPage() {
return (
<Inbox
applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID!}
subscriberId="subscriber-123"
subscriberHash="HMAC_HASH"
/>
);
}
The <Inbox> component is composable. When you pass children, it acts as a context provider and you compose the UI from primitives:
| Component | Purpose |
| --- | --- |
| <Bell /> | Bell icon with unread count |
| <Notifications /> | Notification feed (header + list + footer) |
| <InboxContent /> | Same as <Notifications /> plus the Preferences page |
| <Preferences /> | Standalone preferences panel |
import { Inbox, Bell, Notifications, Preferences } from "@novu/react";
function App() {
return (
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="subscriber-123"
subscriberHash="HMAC_HASH"
>
<Bell />
<Notifications />
<Preferences />
</Inbox>
);
}
Use these primitives to build a custom popover, modal, drawer, or full-page notification experience.
The Inbox is fully themeable via the appearance prop. It supports four keys:
| Key | Purpose |
| --- | --- |
| baseTheme | Apply a predefined theme (e.g. dark) |
| variables | Global design tokens (colors, fonts, radius, severity colors) |
| elements | Per-element styles (style object, class string, or context callback) |
| icons | Replace built-in icons with your own React components |
Styles are auto-injected into <head> (or the shadow root if rendered inside a shadow DOM). When both baseTheme and variables are provided, variables win.
Inspiration: the Inbox Playground showcases pre-styled variants like Notion and Reddit.
import { Inbox } from "@novu/react";
import { dark } from "@novu/react/themes";
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="subscriber-123"
appearance={{ baseTheme: dark }}
/>
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="subscriber-123"
appearance={{
variables: {
colorPrimary: "#0081F1",
colorBackground: "#ffffff",
colorForeground: "#1A1523",
colorPrimaryForeground: "#ffffff",
colorSecondary: "#F1F0EF",
colorCounter: "#E5484D",
colorCounterForeground: "#ffffff",
colorNeutral: "#E0DEDC",
colorShadow: "rgba(0,0,0,0.08)",
fontSize: "14px",
borderRadius: "8px",
colorSeverityHigh: "#E5484D",
colorSeverityMedium: "#F76808",
colorSeverityLow: "#3E63DD",
},
}}
/>
Each element accepts a string of class names, a style object, or a function (context) => string for runtime conditionals.
import inboxStyles from "./inbox.module.css";
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="subscriber-123"
appearance={{
elements: {
bellIcon: ({ unreadCount }) =>
unreadCount.total > 10
? "p-4 bg-white rounded-full [--bell-gradient-end:var(--color-red-500)]"
: "p-4 bg-white rounded-full",
notification: ({ notification }) =>
notification.data?.priority === "high"
? "bg-red-50 ring-1 ring-red-300 rounded-lg"
: "bg-white rounded-lg shadow-sm hover:bg-gray-50",
notificationSubject: { fontWeight: 600 },
notificationBody: inboxStyles.body,
},
}}
/>
To find an element key, inspect the DOM: any class starting with
nv-(visible just before a 🔔 emoji in DevTools) maps to a key inappearance.elements(drop thenv-prefix). TS autocomplete lists all available keys.
Replace any built-in icon by returning a React component from appearance.icons:
import { RiSettings3Fill, RiNotification3Fill } from "react-icons/ri";
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="subscriber-123"
appearance={{
icons: {
bell: () => <RiNotification3Fill />,
cogs: () => <RiSettings3Fill />,
},
}}
/>
Common icon keys: bell, cogs, dots, arrowDown, arrowDropDown, arrowLeft, arrowRight, check, clock, trash, markAsRead, markAsUnread, markAsArchived, markAsUnarchived, email, sms, push, inApp, chat. To find more, inspect classes that start with nv- and contain a 🖼️ emoji.
Notifications and the bell are styled by severity (high, medium, low). Override colors via variables:
Severity is a visual dial only. The workflow-level
critical: trueflag is independent — it changes runtime delivery (bypass preferences, skip digest), not Inbox styling.criticalworkflows that should also stand out visually should setseverity: 'high'explicitly. Seedesign-workflow/references/severity-and-critical.mdfor the full design rules.
appearance: {
variables: {
colorSeverityHigh: "#E5484D",
colorSeverityMedium: "#F76808",
colorSeverityLow: "#3E63DD",
},
}
…or per element:
appearance: {
elements: {
severityHigh__notificationBar: { backgroundColor: "red" },
severityHigh__bellContainer: "ring-2 ring-red-500",
severityGlowHigh__bellSeverityGlow: "bg-red-500",
},
}
By default the bell takes the color of the highest-severity unread notification.
<Inbox
/* ... */
appearance={{ elements: { popoverContent: "novu-popover-content" } }}
/>
.novu-popover-content { max-width: 500px; }
@media (max-width: 768px) { .novu-popover-content { max-width: 350px; } }
@media (max-width: 480px) { .novu-popover-content { max-width: 250px; } }
See Branding & Styling Reference for the full variable list, severity element keys, dynamic callback signatures, and Notion/Reddit-style presets.
Override individual parts of a notification — keep the surrounding chrome (action buttons, hover state, etc.) intact:
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="subscriber-123"
renderBell={(unreadCount) => <MyBell count={unreadCount.total} />}
renderAvatar={(notification) => <Avatar src={notification.avatar} />}
renderSubject={(notification) => <strong>{notification.subject}</strong>}
renderBody={(notification) => <p>{notification.body}</p>}
renderDefaultActions={(notification) => <MyActions notification={notification} />}
renderCustomActions={(notification) => (
<PrimarySecondaryButtons notification={notification} />
)}
/>
Use renderNotification only when you need full control of the item — you'll need to re-implement default actions (mark as read, archive, snooze) yourself.
<Inbox
/* ... */
renderNotification={(notification) => (
<div className="custom-row">
<h3>{notification.subject}</h3>
<p>{notification.body}</p>
</div>
)}
/>
renderNotification receives the full notification — branch on tags, data, severity, or workflow.identifier:
renderNotification={(notification) => {
if (notification.severity === SeverityLevelEnum.HIGH) return <HighPriorityRow notification={notification} />;
if (notification.tags?.includes("billing")) return <BillingRow notification={notification} />;
if (notification.data?.priority === "high") return <UrgentRow notification={notification} />;
return <DefaultRow notification={notification} />;
}}
To render rich HTML in subject / body:
dangerouslySetInnerHTML in a render prop:<Inbox
/* ... */
renderBody={(notification) => (
<div dangerouslySetInnerHTML={{ __html: notification.body }} />
)}
renderSubject={(notification) => (
<span dangerouslySetInnerHTML={{ __html: notification.subject }} />
)}
/>
Only enable this if you fully control the trigger payload — raw HTML opens an XSS surface area.
Hook the Inbox into your router. Novu calls routerPush with the redirect.url defined in your workflow:
import { useRouter } from "next/navigation";
const router = useRouter();
<Inbox
/* ... */
routerPush={(path) => router.push(path)}
onNotificationClick={(notification) => track("inbox_notification_click", { id: notification.id })}
onPrimaryActionClick={(notification) => doSomething(notification.primaryAction)}
onSecondaryActionClick={(notification) => doSomethingElse(notification.secondaryAction)}
/>
Works with React Router (useNavigate()), Remix (useNavigate()), Gatsby (navigate()), and any custom router.
See Personalization Reference for full render-prop signatures, renderCustomActions styling examples, popover composition with Radix / shadcn Drawer, and conditional UI patterns.
Group notifications into tabs by tags, severity, or data properties:
import { Inbox, SeverityLevelEnum } from "@novu/react";
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="subscriber-123"
tabs={[
{ label: "All", filter: { tags: [] } },
{ label: "Promotions", filter: { tags: ["promotions"] } },
{ label: "Security", filter: { tags: ["security", "alert"] } },
{ label: "Critical", filter: { severity: SeverityLevelEnum.HIGH } },
{ label: "High Priority", filter: { data: { priority: "high" } } },
{
label: "Billing",
filter: { tags: ["billing"], data: { entity: "invoice" } },
},
]}
/>
OR logic.HIGH, MEDIUM, LOW).data comes from the data object defined per In-App step.Use the useCounts hook to render unread counts per tab.
Use Contexts to scope the Inbox to a tenant, workspace, or feature area. The Inbox shows only notifications whose trigger context matches the Inbox context exactly.
await novu.trigger({
workflowId: "invoice-paid",
to: { subscriberId: "user-123" },
payload: { amount: "$250" },
context: {
tenant: {
id: "acme-corp",
data: { name: "Acme Corporation", plan: "enterprise" },
},
},
});
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="user-123"
subscriberHash="HMAC_HASH"
context={{
tenant: {
id: "acme-corp",
data: { name: "Acme Corporation", plan: "enterprise" },
},
}}
/>
contextHashBecause context is set client-side, a hostile user could swap tenant IDs. Generate an HMAC hash of the canonicalized context server-side:
import { createHmac } from "crypto";
import { canonicalize } from "@tufjs/canonical-json";
const context = {
tenant: { id: "acme-corp", data: { name: "Acme Corporation", plan: "enterprise" } },
};
const contextHash = createHmac("sha256", process.env.NOVU_SECRET_KEY!)
.update(canonicalize(context))
.digest("hex");
Pass it alongside the context:
<Inbox
/* ... */
context={context}
contextHash={contextHash}
/>
| Workflow Context | Inbox Context | Displayed? |
| --- | --- | --- |
| { tenant: "acme" } | { tenant: "acme" } | ✅ |
| {} | {} | ✅ |
| { tenant: "acme" } | {} | ❌ |
| {} | { tenant: "acme" } | ❌ |
| { tenant: "acme" } | { tenant: "globex" } | ❌ |
Context that doesn't yet exist in Novu is auto-created. Existing context data is not auto-updated to prevent overwrites.
See Multi-Tenancy Reference for full setup, dashboard management, and dynamic content rendering with {{context}}.
Each In-App step supports a custom data object — up to 10 scalar key-value pairs (string, number, boolean, null; strings ≤ 256 chars) defined in the workflow editor. Values can be static ("status": "merged") or dynamic ("firstName": "{{subscriber.firstName}}").
Access it client-side as notification.data and use it for render decisions, conditional styling, and tab filtering.
<Inbox
/* ... */
renderNotification={(notification) => (
<div>
<span>{notification.data?.emoji}</span>
<strong>{notification.data?.firstName}</strong>
<p>{notification.body}</p>
</div>
)}
/>
Type the data object globally for autocomplete:
declare global {
interface NotificationData {
reactionType?: string;
entityId?: string;
userName?: string;
}
}
Don't store secrets in
data— it's returned to the client. Never spread the entire trigger payload intodata.
Mount the notification feed inside any popover, drawer, or page layout. Use <Bell /> (or your own trigger) plus <Notifications /> or <InboxContent />:
import { Inbox, InboxContent, Bell } from "@novu/react";
import { Popover, PopoverTrigger, PopoverContent } from "@radix-ui/react-popover";
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="subscriber-123"
>
<Popover>
<PopoverTrigger>
<Bell />
</PopoverTrigger>
<PopoverContent className="h-[600px] w-[400px] p-0">
<InboxContent />
</PopoverContent>
</Popover>
</Inbox>
The same pattern works with shadcn <Drawer>, Headless UI, or a route-level page (mount <InboxContent /> directly without any popover). All customization props (appearance, localization, tabs, routerPush, render props) flow through the <Inbox> provider.
Override Inbox UI text — useful for multi-language apps or matching your product voice:
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="subscriber-123"
localization={{
locale: "en-US",
"inbox.filters.labels.default": "Notifications",
"inbox.filters.dropdownOptions.unread": "Unread only",
"notifications.emptyNotice": "You're all caught up.",
"notifications.actions.readAll": "Mark all as read",
"notification.actions.archive.tooltip": "Move to archive",
"preferences.title": "Notification Preferences",
dynamic: {
"new-comment-on-post": "Post comments",
"new-follower-digest": "New Follower Updates",
},
}}
/>
dynamic map to localize workflow names shown in the Preferences UI.defaultLocalization.ts.Required in production to prevent subscriber impersonation. See https://docs.novu.co/platform/inbox/prepare-for-production for the full guide.
import { createHmac } from "crypto";
const subscriberHash = createHmac("sha256", process.env.NOVU_SECRET_KEY!)
.update(subscriberId)
.digest("hex");
import hmac, hashlib
subscriber_hash = hmac.new(
NOVU_SECRET_KEY.encode(),
subscriber_id.encode(),
hashlib.sha256,
).hexdigest()
<Inbox
applicationIdentifier="YOUR_NOVU_APP_ID"
subscriberId="subscriber-123"
subscriberHash={subscriberHash}
/>
If you also pass a context, generate a contextHash (see Multi-Tenancy).
applicationIdentifier is NOT the same as NOVU_SECRET_KEY — the app ID is a public identifier safe for client-side use. The secret key is server-only.inApp step — if your workflow doesn't include step.inApp(), nothing appears."use client" is required in Next.js App Router — the Inbox component is client-side only.@novu/react vs @novu/nextjs — use @novu/nextjs for Next.js apps (handles SSR edge cases), @novu/react for all other React apps.variables override baseTheme — when both are set in appearance, variables win. Set variables in dark/light themes intentionally.(context) => string returns class names, not style objects. For style objects use a static value.context={{}} to the Inbox hides any notification triggered with a non-empty context, and vice-versa.notification.data — it's sent to the client.renderNotification removes default actions — use granular render props (renderSubject, renderBody, renderAvatar, renderDefaultActions, renderCustomActions) when you want to keep mark-as-read / archive / snooze affordances.dangerouslySetInnerHTML in a render prop. Either alone has no effect.data-ai
Trigger Novu notification workflows to send messages across email, SMS, push, chat, and in-app channels. Supports single triggers, bulk triggers, broadcast to all subscribers, topic-based targeting, and cancellation. Use when sending transactional notifications, alerts, or any event-driven messages.
testing
Create, update, search, and delete subscribers in Novu. Manage topics for group-based notification targeting. Set subscriber credentials for push and chat channels. Use when managing notification recipients, creating subscriber records, organizing subscribers into topics, or configuring channel-specific credentials.
development
Configure notification preferences in Novu at the workflow and subscriber level. Set default channel preferences (email, SMS, push, chat, in-app), mark preferences as read-only or subscriber-editable, and manage subscriber-specific overrides. Use when setting up notification opt-in/opt-out, configuring per-channel delivery preferences, or building a preferences management UI.
tools
Build code-first notification workflows with @novu/framework. Use when defining workflows in TypeScript (Zod / JSON Schema / Class Validator), composing channel steps (email, SMS, push, chat, in-app) with action steps (delay, digest, custom), exposing Step Controls for non-technical teammates, rendering React/Vue/Svelte Email templates, hosting the Bridge Endpoint inside Next.js, Express, NestJS, Remix, Nuxt, SvelteKit, H3, or AWS Lambda, syncing to Novu Cloud via CLI / GitHub Actions, securing production with HMAC, or implementing translations, hydration, multi-channel orchestration, and LLM-powered notification logic in code.