Notifications

Real-time notification system powered by Novu

Overview

The notification system integrates Novu, an open-source notification infrastructure, to deliver real-time in-app notifications to users. The system automatically syncs with Clerk authentication and matches the Material-UI theme for a seamless user experience.

Architecture

The notification system consists of a single client component that renders a notification bell in the app header. When clicked, it opens an inbox dropdown showing all user notifications with read/unread tracking.

File Structure

Configuration

Environment Variables

Add your Novu application identifier to .env.local:

.env.local
NEXT_PUBLIC_NOVU_APPLICATION_IDENTIFIER=your-app-identifier

Get this identifier from Novu Dashboard → Settings → API Keys.

The NEXT_PUBLIC_ prefix is required because Novu renders client-side and needs access to this value in the browser.

Implementation

Notification Inbox Component

The inbox component integrates Clerk authentication with Novu's notification system. It automatically creates or updates the subscriber profile whenever the user logs in.

components/notifications/notification-inbox.tsx
"use client"

import { useUser } from "@clerk/nextjs"
import { Inbox } from "@novu/nextjs"
import { Bell } from "@phosphor-icons/react"

export function NotificationInbox() {
  const { user, isLoaded } = useUser()
  const applicationIdentifier = process.env.NEXT_PUBLIC_NOVU_APPLICATION_IDENTIFIER

  if (!applicationIdentifier) {
    console.error("NEXT_PUBLIC_NOVU_APPLICATION_IDENTIFIER is not configured")
    return null
  }

  if (!isLoaded || !user) {
    return null
  }

  return (
    <Inbox
      applicationIdentifier={applicationIdentifier}
      subscriber={{
        subscriberId: user.id,
        firstName: user.firstName ?? undefined,
        lastName: user.lastName ?? undefined,
        email: user.primaryEmailAddress?.emailAddress,
        avatar: user.imageUrl,
      }}
      appearance={{
        variables: {
          colorPrimary: "var(--mui-palette-primary-main)",
          colorPrimaryForeground: "var(--mui-palette-primary-contrastText)",
          colorSecondary: "var(--mui-palette-secondary-main)",
          colorSecondaryForeground: "var(--mui-palette-secondary-contrastText)",
          colorBackground: "var(--mui-palette-background-paper)",
          colorForeground: "var(--mui-palette-text-primary)",
          colorNeutral: "var(--mui-palette-divider)",
          fontSize: "14px",
        },
        icons: {
          bell: () => <Bell size={24} />,
        },
      }}
    />
  )
}

Key Implementation Details

Layout Integration

The notification inbox is placed in the app header, next to the user menu:

components/dashboard/layout/vertical/vertical-layout.tsx
import { NotificationInbox } from "@/components/notifications/notification-inbox"

export function VerticalLayout({ children }) {
  return (
    <Box>
      <AppBar>
        <Toolbar>
          <Logo />
          <Navigation />
          <Box sx={{ ml: "auto", display: "flex", gap: 2 }}>
            <NotificationInbox />
            <UserButton />
          </Toolbar>
        </AppBar>
        <Main>{children}</Main>
      </AppBar>
    </Box>
  )
}

Backend Integration

Creating Workflows

Workflows define the notification templates and delivery channels. Create them in the Novu Dashboard:

Go to Novu Dashboard → Workflows → Create Workflow

Configure Trigger

Set a unique trigger ID (e.g., guest-disruption-alert) that your backend will use to send notifications

Select Channels

Choose delivery channels: In-app (required), Email, SMS, Push (optional)

Design Template

Create the notification content using Handlebars syntax with payload variables

Sending Notifications

From your backend API, trigger notifications using the Novu Node.js SDK:

Backend API
import { Novu } from "@novu/node"

const novu = new Novu(process.env.NOVU_API_KEY)

await novu.trigger("guest-disruption-alert", {
  to: {
    subscriberId: userId,
  },
  payload: {
    title: "Flight Schedule Changed",
    message: "Your flight LAX-JFK on 2024-01-15 has been rescheduled",
    route: "LAX-JFK",
    flightNumber: "OD123",
    link: "/connections/guest-disruptions",
  },
})

Notification Types

Notify users about schedule upload status and processing results:

await novu.trigger("schedule-update", {
  to: { subscriberId: userId },
  payload: {
    title: "Schedule Updated",
    message: "New schedule uploaded successfully",
    scheduleId: "SCH456",
    timestamp: new Date().toISOString(),
    link: "/admin/schedules",
  },
})

Use case: Confirm successful schedule uploads or alert about processing errors.

Notify users about system-wide events or maintenance:

await novu.trigger("system-alert", {
  to: { subscriberId: userId },
  payload: {
    title: "System Maintenance",
    message: "Scheduled maintenance on Jan 20, 2024 at 2:00 AM UTC",
    severity: "warning",
    link: "/settings",
  },
})

Use case: Communicate planned downtime or important system updates.

Styling & Theming

Material-UI Integration

The inbox automatically inherits your Material-UI theme through CSS variables. This approach ensures the notification UI stays consistent with the rest of the application, including automatic light/dark mode support.

appearance={{
  variables: {
    colorPrimary: "var(--mui-palette-primary-main)",
    colorBackground: "var(--mui-palette-background-paper)",
    colorForeground: "var(--mui-palette-text-primary)",
    colorNeutral: "var(--mui-palette-divider)",
    fontSize: "14px",
  },
}}

Why CSS variables? Material-UI exposes its theme as CSS variables, allowing Novu to reactively update when the theme changes (e.g., switching to dark mode).

Custom Styling

For advanced customization, target Novu's data attributes in your global CSS:

app/globals.css
[data-novu-component="inbox"] {
  /* Customize inbox container */
  max-width: 400px;
}

[data-novu-component="notification-item"] {
  /* Customize individual notifications */
  border-radius: 8px;
}

[data-novu-component="notification-item"][data-read="false"] {
  /* Style unread notifications */
  background-color: var(--mui-palette-action-hover);
}

Real-Time Updates

Novu uses WebSockets to deliver notifications in real-time. When a notification is triggered from the backend, it appears instantly in the user's inbox without requiring a page refresh or polling.

How It Works

The WebSocket connection is established automatically when the <Inbox> component mounts. No additional configuration is needed.

Testing

Local Development

Test notifications locally by creating a test endpoint:

app/api/notifications/test/route.ts
import { Novu } from "@novu/node"

export async function POST(request: Request) {
  const { userId, type } = await request.json()
  const novu = new Novu(process.env.NOVU_API_KEY)

  await novu.trigger(type, {
    to: { subscriberId: userId },
    payload: {
      title: "Test Notification",
      message: "This is a test notification",
      link: "/",
    },
  })

  return Response.json({ success: true })
}

Novu Dashboard Testing

Open Workflow

Go to Workflows → Select your workflow

Click Test Workflow

Enter test payload data

Send Test

Click "Send Test" button

Check Inbox

Open your app and verify the notification appears

Monitoring

Analytics

Track notification performance in the Novu Dashboard:

MetricDescriptionWhy It Matters
Delivery Rate% successfully deliveredIdentifies delivery issues
Open Rate% of notifications openedMeasures user engagement
Click-Through Rate% of links clickedTracks notification effectiveness
ErrorsFailed deliveriesHelps debug issues

Activity Feed

The Activity Feed shows real-time notification events. Filter by subscriber, workflow, status, or date range to debug issues or analyze user behavior.

Best Practices

Troubleshooting

Advanced Features

Custom Notification Actions

Add custom action buttons to notifications:

<Inbox
  applicationIdentifier={applicationIdentifier}
  subscriber={subscriber}
  notificationActions={(notification) => [
    {
      label: "View Details",
      onClick: () => router.push(notification.data.link),
    },
    {
      label: "Dismiss",
      onClick: () => markAsRead(notification.id),
    },
  ]}
/>

Multi-Tenant Support

For applications with multiple organizations, include the organization ID in the subscriber ID:

subscriber={{
  subscriberId: `${organizationId}:${userId}`,
  data: {
    organizationId: organizationId,
  },
}}

This allows you to send notifications to all users in an organization or filter by organization in the backend.


Next: Explore the Deployment Guide to learn about CI/CD pipelines and deployment strategies.