--- name: novu-inbox-integration description: 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. inputs: - name: NOVU_APPLICATION_IDENTIFIER description: "Application identifier for client-side Inbox integration. Found in dashboard integration settings." required: true type: string --- # 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 ```bash npm install @novu/react ``` ```tsx 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 ```bash npm install @novu/nextjs ``` ### App Router ```tsx // 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 ```tsx 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 | ```tsx 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](https://inbox.novu.co) showcases pre-styled variants like Notion and Reddit. ### Dark mode (and other base themes) ```tsx import { Inbox } from "@novu/react"; import { dark } from "@novu/react/themes"; ``` ### Global variables ```tsx ``` ### 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. ```tsx 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`: ```tsx 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. ```tsx appearance: { variables: { colorSeverityHigh: "#E5484D", colorSeverityMedium: "#F76808", colorSeverityLow: "#3E63DD", }, } ``` …or per element: ```tsx 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 ```tsx ``` ```css .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: ```tsx } 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. ```tsx (

{notification.subject}

{notification.body}

)} /> ``` ### Conditional display `renderNotification` receives the full notification β€” branch on `tags`, `data`, `severity`, or `workflow.identifier`: ```tsx 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`: 1. Disable **Disable content sanitization** in the In-App step in your workflow. 2. Render with `dangerouslySetInnerHTML` in a render prop: ```tsx (
)} 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: ```tsx 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**: ```tsx import { Inbox, SeverityLevelEnum } from "@novu/react"; ``` - **Tags** are workflow-level β€” assign them in the workflow editor. Multiple tags use `OR` logic. - **Severity** comes from the In-App step's severity setting (`HIGH`, `MEDIUM`, `LOW`). - **`data`** comes from the [data object](#data-object) defined per In-App step. Use the [`useCounts` hook](https://docs.novu.co/platform/sdks/react/hooks/use-counts) 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 ```typescript 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 ```tsx ``` ### 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: ```typescript 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`: ```tsx ``` ### 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. ```tsx (
{notification.data?.emoji} {notification.data?.firstName}

{notification.body}

)} /> ``` Type the data object globally for autocomplete: ```ts 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 ``: ```tsx 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: ```tsx ``` - Localization changes UI text only. To translate notification *content*, use [Workflow Translations](https://docs.novu.co/platform/workflow/advanced-features/translations). - Use the `dynamic` map to localize workflow names shown in the Preferences UI. - The full key list lives in [`defaultLocalization.ts`](https://github.com/novuhq/novu/blob/next/packages/js/src/ui/config/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) ```typescript import { createHmac } from "crypto"; const subscriberHash = createHmac("sha256", process.env.NOVU_SECRET_KEY!) .update(subscriberId) .digest("hex"); ``` ### Python ```python import hmac, hashlib subscriber_hash = hmac.new( NOVU_SECRET_KEY.encode(), subscriber_id.encode(), hashlib.sha256, ).hexdigest() ``` ### Pass to the component ```tsx ``` If you also pass a `context`, generate a `contextHash` (see [Multi-Tenancy](#multi-tenancy-with-contexts)). ## Common Pitfalls 1. **`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. 2. **HMAC hash is mandatory in production** β€” without it, anyone can impersonate a subscriber by guessing their ID. 3. **The Inbox only shows notifications from workflows with an `inApp` step** β€” if your workflow doesn't include `step.inApp()`, nothing appears. 4. **`"use client"` is required in Next.js App Router** β€” the Inbox component is client-side only. 5. **Real-time updates are automatic** β€” the Inbox uses WebSockets internally. No additional setup needed. 6. **`@novu/react` vs `@novu/nextjs`** β€” use `@novu/nextjs` for Next.js apps (handles SSR edge cases), `@novu/react` for all other React apps. 7. **`variables` override `baseTheme`** β€” when both are set in `appearance`, variables win. Set variables in dark/light themes intentionally. 8. **Element callbacks return strings** β€” `(context) => string` returns class names, not style objects. For style objects use a static value. 9. **Context filtering is exact-match** β€” passing `context={{}}` to the Inbox hides any notification triggered with a non-empty context, and vice-versa. 10. **Don't store secrets in `notification.data`** β€” it's sent to the client. 11. **`renderNotification` removes default actions** β€” use granular render props (`renderSubject`, `renderBody`, `renderAvatar`, `renderDefaultActions`, `renderCustomActions`) when you want to keep mark-as-read / archive / snooze affordances. 12. **HTML rendering requires both steps** β€” disabling sanitization in the workflow *and* using `dangerouslySetInnerHTML` in 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)