Claude Code for Novu: Open-Source Notification Infrastructure — Claude Skills 360 Blog
Blog / Backend / Claude Code for Novu: Open-Source Notification Infrastructure
Backend

Claude Code for Novu: Open-Source Notification Infrastructure

Published: May 23, 2027
Read time: 6 min read
By: Claude Skills 360

Novu is an open-source notification infrastructure platform — new Novu(apiKey) creates the SDK. novu.trigger(workflowId, { to: { subscriberId }, payload }) fires a notification workflow. novu.subscribers.identify(id, { email, firstName, lastName, phone }) creates or updates a subscriber. novu.subscribers.setCredentials(id, "fcm", { deviceTokens: [token] }) adds push notification credentials. The @novu/notification-center React package embeds an inbox UI. Workflows compose steps: email, sms, push, in_app, chat — each configured with a template. digest step batches events over a time window. delay step waits before the next step. Subscriber preferences let users control which notifications they receive. Novu has a cloud offering at novu.co and a self-hosted Docker image. novu.triggerBulk sends to many subscribers. Claude Code generates Novu notification workflows, digest batching, in-app inbox components, and multi-channel delivery.

CLAUDE.md for Novu

## Novu Stack
- Version: @novu/node >= 2.6, @novu/notification-center >= 2.1
- Init: const novu = new Novu(process.env.NOVU_API_KEY!)
- Trigger: await novu.trigger("workflow-id", { to: { subscriberId: userId }, payload: { message, link } })
- Identify: await novu.subscribers.identify(userId, { email, firstName, phone })
- Bulk: await novu.bulkTrigger([{ name: "workflow", to: { subscriberId }, payload }])
- Inbox: <NovuProvider subscriberId={userId} applicationIdentifier={NOVU_APP_ID}><PopoverNotificationCenter /></NovuProvider>

Novu Server Utilities

// lib/notifications/novu.ts — Novu notification utilities
import { Novu } from "@novu/node"
import { db } from "@/lib/db"
import { users } from "@/lib/db/schema"
import { eq } from "drizzle-orm"

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

// Workflow IDs — defined in Novu dashboard or via code
const WORKFLOWS = {
  welcome: "welcome-email",
  passwordReset: "password-reset",
  orderConfirmed: "order-confirmed",
  newComment: "new-comment",
  weeklyDigest: "weekly-digest",
  mentionAlert: "mention-alert",
} as const

// ── Subscriber management ──────────────────────────────────────────────────
export async function syncSubscriber(userId: string): Promise<void> {
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
    columns: { id: true, email: true, name: true, phone: true, avatarUrl: true },
  })

  if (!user) return

  const [firstName, ...rest] = (user.name ?? "").split(" ")

  await novu.subscribers.identify(userId, {
    email: user.email,
    firstName,
    lastName: rest.join(" ") || undefined,
    phone: user.phone ?? undefined,
    avatar: user.avatarUrl ?? undefined,
    data: { userId },
  })
}

// ── Notification triggers ──────────────────────────────────────────────────

export async function sendWelcomeNotification(userId: string, appName: string): Promise<void> {
  await novu.trigger(WORKFLOWS.welcome, {
    to: { subscriberId: userId },
    payload: {
      appName,
      loginUrl: `${process.env.APP_URL}/sign-in`,
    },
  })
}

export async function sendPasswordResetNotification(
  userId: string,
  resetUrl: string,
  expiresInMinutes = 60,
): Promise<void> {
  await novu.trigger(WORKFLOWS.passwordReset, {
    to: { subscriberId: userId },
    payload: {
      resetUrl,
      expiresInMinutes: String(expiresInMinutes),
    },
  })
}

export async function sendOrderConfirmation(userId: string, order: {
  id: string
  total: string
  items: Array<{ name: string; quantity: number; price: string }>
}): Promise<void> {
  await novu.trigger(WORKFLOWS.orderConfirmed, {
    to: { subscriberId: userId },
    payload: {
      orderId: order.id,
      orderTotal: order.total,
      orderItems: order.items,
      orderUrl: `${process.env.APP_URL}/orders/${order.id}`,
    },
  })
}

export async function sendMentionAlert(params: {
  mentionedUserId: string
  mentionedByName: string
  contextUrl: string
  preview: string
}): Promise<void> {
  await novu.trigger(WORKFLOWS.mentionAlert, {
    to: { subscriberId: params.mentionedUserId },
    payload: {
      mentionedByName: params.mentionedByName,
      contextUrl: params.contextUrl,
      preview: params.preview.slice(0, 200),
    },
  })
}

// ── Bulk notification ──────────────────────────────────────────────────────
export async function sendWeeklyDigest(userIds: string[]): Promise<void> {
  // Novu bulk API sends up to 100 at once
  const chunks = []
  for (let i = 0; i < userIds.length; i += 100) {
    chunks.push(userIds.slice(i, i + 100))
  }

  for (const chunk of chunks) {
    await novu.bulkTrigger(
      chunk.map(userId => ({
        name: WORKFLOWS.weeklyDigest,
        to: { subscriberId: userId },
        payload: {
          digestUrl: `${process.env.APP_URL}/digest`,
          unsubscribeUrl: `${process.env.APP_URL}/unsubscribe/${userId}`,
        },
      })),
    )
  }
}

// Register FCM push token for a user
export async function registerPushToken(userId: string, fcmToken: string): Promise<void> {
  await novu.subscribers.setCredentials(userId, "fcm", {
    deviceTokens: [fcmToken],
  })
}

In-App Notification Center

// components/notifications/InAppInbox.tsx — Novu notification center
"use client"
import {
  NovuProvider,
  PopoverNotificationCenter,
  NotificationBell,
  useNotifications,
  type IMessage,
} from "@novu/notification-center"

const NOVU_APP_ID = process.env.NEXT_PUBLIC_NOVU_APP_ID!

interface InboxProps {
  userId: string
  subscriberHash?: string  // HMAC for production security
}

export function InAppInbox({ userId, subscriberHash }: InboxProps) {
  return (
    <NovuProvider
      subscriberId={userId}
      applicationIdentifier={NOVU_APP_ID}
      subscriberHash={subscriberHash}
      initialFetchingStrategy={{ fetchNotifications: true, fetchUserPreferences: true }}
    >
      <PopoverNotificationCenter
        colorScheme="auto"
        onNotificationClick={(notification) => {
          const url = notification.cta?.data?.url
          if (url) window.location.href = url
        }}
        onActionButtonClick={(notification, actionButtonType) => {
          console.log("Action:", actionButtonType, notification.id)
        }}
        listItem={(notification, handleActionButtonClick, handleNotificationClick) => (
          <CustomNotificationItem
            notification={notification}
            onClick={handleNotificationClick}
          />
        )}
      >
        {({ unseenCount }) => (
          <NotificationBell unseenCount={unseenCount} />
        )}
      </PopoverNotificationCenter>
    </NovuProvider>
  )
}

function CustomNotificationItem({
  notification,
  onClick,
}: {
  notification: IMessage
  onClick: (n: IMessage) => void
}) {
  return (
    <div
      onClick={() => onClick(notification)}
      className={`flex gap-3 p-3 cursor-pointer hover:bg-muted/50 rounded-lg ${!notification.seen ? "bg-blue-50/50" : ""}`}
    >
      {notification.actor?.avatar && (
        <img src={notification.actor.avatar} alt="" className="size-9 rounded-full flex-shrink-0 object-cover" />
      )}
      <div className="flex-1 min-w-0">
        <p className="text-sm line-clamp-2">{notification.content as string}</p>
        <p className="text-xs text-muted-foreground mt-0.5">
          {new Date(notification.createdAt).toRelativeString?.() ??
            new Date(notification.createdAt).toLocaleDateString()}
        </p>
      </div>
      {!notification.seen && (
        <div className="size-2 rounded-full bg-blue-500 flex-shrink-0 mt-1.5" />
      )}
    </div>
  )
}

Subscriber Preferences API

// app/api/notifications/preferences/route.ts — manage notification preferences
import { NextRequest, NextResponse } from "next/server"
import { novu } from "@/lib/notifications/novu"
import { auth } from "@clerk/nextjs/server"

// GET preferences
export async function GET(request: NextRequest) {
  const { userId } = await auth()
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })

  const preferences = await novu.subscribers.getPreference(userId)
  return NextResponse.json({ preferences })
}

// PATCH update channel preference
export async function PATCH(request: NextRequest) {
  const { userId } = await auth()
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })

  const { templateId, channel, enabled } = await request.json()

  await novu.subscribers.updatePreference(userId, templateId, {
    channel: {
      type: channel,
      enabled,
    },
  })

  return NextResponse.json({ ok: true })
}

For the Knock alternative when a more developer-focused notification platform with per-workflow dashboards, multi-tenant support for SaaS applications, frequency capping, and a richer in-app feed API is preferred — Knock has a more polished dashboard and better multi-tenant isolation, while Novu is fully open-source and self-hostable, see the Knock guide. For the Resend + React Email alternative when only transactional email (no in-app, SMS, or push) is needed without a full notification orchestration platform — Resend with React Email templates is simpler and more cost-effective when you only need email delivery, see the Resend guide. The Claude Skills 360 bundle includes Novu skill sets covering workflows, in-app inbox, and multi-channel delivery. Start with the free tier to try notification infrastructure generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free