Install
$ agentstack add skill-novuhq-skills-inbox-integration ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Inbox Integration
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.
Packages
| Package | Use For | | --- | --- | | @novu/react | React 18/19 applications | | @novu/nextjs | Next.js (App Router + Pages Router) | | @novu/js | Vanilla JavaScript / non-React frameworks |
React Quick Start
npm install @novu/react
import { Inbox } from "@novu/react";
function App() {
return (
);
}
This renders a bell icon with unread count. Clicking it opens a popover with the notification feed.
Next.js
npm install @novu/nextjs
App Router
// components/NotificationInbox.tsx
"use client";
import { Inbox } from "@novu/nextjs";
export function NotificationInbox() {
return (
);
}
Important: The Inbox is a client component — use "use client" directive in Next.js App Router.
Pages Router
import { Inbox } from "@novu/nextjs";
export default function NotificationsPage() {
return (
);
}
Composable Components
The `` component is composable. When you pass children, it acts as a context provider and you compose the UI from primitives:
| Component | Purpose | | --- | --- | | ` | Bell icon with unread count | | | Notification feed (header + list + footer) | | | Same as plus the Preferences page | | ` | Standalone preferences panel |
import { Inbox, Bell, Notifications, Preferences } from "@novu/react";
function App() {
return (
);
}
Use these primitives to build a custom popover, modal, drawer, or full-page notification experience.
Branding the Inbox
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 ` (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.
Dark mode (and other base themes)
import { Inbox } from "@novu/react";
import { dark } from "@novu/react/themes";
Global variables
Element-level styling (Tailwind, CSS Modules, inline styles)
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";
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 in appearance.elements (drop the nv- prefix). TS autocomplete lists all available keys.
Custom icons
Replace any built-in icon by returning a React component from appearance.icons:
import { RiSettings3Fill, RiNotification3Fill } from "react-icons/ri";
,
cogs: () => ,
},
}}
/>
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.
Severity styling
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: true flag is independent — it changes runtime delivery (bypass preferences, skip digest), not Inbox styling. critical workflows that should also stand out visually should set severity: 'high' explicitly. See [design-workflow/references/severity-and-critical.md](../design-workflow/references/severity-and-critical.md) for 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.
Responsive Inbox
.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](./references/branding-and-styling.md) for the full variable list, severity element keys, dynamic callback signatures, and Notion/Reddit-style presets.
Personalization
Render props
Override individual parts of a notification — keep the surrounding chrome (action buttons, hover state, etc.) intact:
}
renderAvatar={(notification) => }
renderSubject={(notification) => {notification.subject}}
renderBody={(notification) => {notification.body}}
renderDefaultActions={(notification) => }
renderCustomActions={(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.
(
{notification.subject}
{notification.body}
)}
/>
Conditional display
renderNotification receives the full notification — branch on tags, data, severity, or workflow.identifier:
renderNotification={(notification) => {
if (notification.severity === SeverityLevelEnum.HIGH) return ;
if (notification.tags?.includes("billing")) return ;
if (notification.data?.priority === "high") return ;
return ;
}}
HTML in notification content
To render rich HTML in subject / body:
- Disable Disable content sanitization in the In-App step in your workflow.
- Render with
dangerouslySetInnerHTMLin a render prop:
(
)}
renderSubject={(notification) => (
)}
/>
> Only enable this if you fully control the trigger payload — raw HTML opens an XSS surface area.
Notification click behavior
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();
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](./references/personalization.md) for full render-prop signatures, renderCustomActions styling examples, popover composition with Radix / shadcn Drawer, and conditional UI patterns.
Tabs
Group notifications into tabs by tags, severity, or data properties:
import { Inbox, SeverityLevelEnum } from "@novu/react";
- Tags are workflow-level — assign them in the workflow editor. Multiple tags use
ORlogic. - Severity comes from the In-App step's severity setting (
HIGH,MEDIUM,LOW). datacomes from the [data object](#data-object) defined per In-App step.
Use the useCounts hook to render unread counts per tab.
Multi-Tenancy with Contexts
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.
1. Trigger workflows with context
await novu.trigger({
workflowId: "invoice-paid",
to: { subscriberId: "user-123" },
payload: { amount: "$250" },
context: {
tenant: {
id: "acme-corp",
data: { name: "Acme Corporation", plan: "enterprise" },
},
},
});
2. Pass the matching context to the Inbox
3. Secure the context with contextHash
Because 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:
Context match rules
| 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](./references/multi-tenancy.md) for full setup, dashboard management, and dynamic content rendering with {{context}}.
Data Object
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.
(
{notification.data?.emoji}
{notification.data?.firstName}
{notification.body}
)}
/>
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 into data.
Custom Popover
Mount the notification feed inside any popover, drawer, or page layout. Use ` (or your own trigger) plus or `:
import { Inbox, InboxContent, Bell } from "@novu/react";
import { Popover, PopoverTrigger, PopoverContent } from "@radix-ui/react-popover";
The same pattern works with shadcn `, Headless UI, or a route-level page (mount directly without any popover). All customization props (appearance, localization, tabs, routerPush, render props) flow through the ` provider.
Localization
Override Inbox UI text — useful for multi-language apps or matching your product voice:
- Localization changes UI text only. To translate notification content, use Workflow Translations.
- Use the
dynamicmap to localize workflow names shown in the Preferences UI. - The full key list lives in
defaultLocalization.ts.
HMAC Authentication
Required in production to prevent subscriber impersonation. See https://docs.novu.co/platform/inbox/prepare-for-production for the full guide.
Generate the hash (server-side)
import { createHmac } from "crypto";
const subscriberHash = createHmac("sha256", process.env.NOVU_SECRET_KEY!)
.update(subscriberId)
.digest("hex");
Python
import hmac, hashlib
subscriber_hash = hmac.new(
NOVU_SECRET_KEY.encode(),
subscriber_id.encode(),
hashlib.sha256,
).hexdigest()
Pass to the component
If you also pass a context, generate a contextHash (see [Multi-Tenancy](#multi-tenancy-with-contexts)).
Common Pitfalls
applicationIdentifieris NOT the same asNOVU_SECRET_KEY— the app ID is a public identifier safe for client-side use. The secret key is server-only.- HMAC hash is mandatory in production — without it, anyone can impersonate a subscriber by guessing their ID.
- The Inbox only shows notifications from workflows with an
inAppstep — if your workflow doesn't includestep.inApp(), nothing appears. "use client"is required in Next.js App Router — the Inbox component is client-side only.- Real-time updates are automatic — the Inbox uses WebSockets internally. No additional setup needed.
@novu/reactvs@novu/nextjs— use@novu/nextjsfor Next.js apps (handles SSR edge cases),@novu/reactfor all other React apps.variablesoverridebaseTheme— when both are set inappearance, variables win. Set variables in dark/light themes intentionally.- Element callbacks return strings —
(context) => stringreturns class names, not style objects. For style objects use a static value. - Context filtering is exact-match — passing
context={{}}to the Inbox hides any notification triggered with a non-empty context, and vice-versa. - Don't store secrets in
notification.data— it's sent to the client. renderNotificationremoves default actions — use granular render props (renderSubject,renderBody,renderAvatar,renderDefaultActions,renderCustomActions) when you want to keep mark-as-read / archive / snooze affordances.- HTML rendering requires both steps — disabling sanitization in the workflow and using
dangerouslySetInnerHTMLin a render prop. Either alone has no effect.
References
- [Branding & Styling](./references/branding-and-styling.md) — full appearance API: themes, variables, elements, icons, severity, dynamic callbacks
- [Personalization](./references/personalization.md) — render props, custom popover (Radix, shadcn Drawer), conditional display, click handlers
- [Multi-Tenancy with Contexts](./references/multi-tenancy.md) — context-based isolation, securing contextHash, dynamic templates
- [React Inbox Examples](./references/react-inbox-examples.md)
- [Next.js Inbox Examples](./references/nextjs-inbox-examples.md)
- [Headless Inbox (Vanilla JS)](./references/headless-inbox-examples.md)
- [Security (HMAC)](./references/security.md)
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: novuhq
- Source: novuhq/skills
- License: MIT
- Homepage: https://novu.co
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.