Loading...
Loading...
> Real-time notifications with database persistence, bell icon badge, and read/unread state management.
The notifications system combines real-time delivery via Pusher with database persistence for a complete notification experience. Users receive instant notifications with a visual bell icon indicator and can manage their notification history.
Notification model in Prisma schema
1// prisma/schema.prisma2model Notification {3 id String @id @default(cuid())4 userId String5 user User @relation(fields: [userId], references: [id], onDelete: Cascade)67 type String // "info", "success", "warning", "error"8 title String9 body String?10 link String? // Optional action link1112 read Boolean @default(false)13 readAt DateTime?1415 createdAt DateTime @default(now())1617 @@index([userId, read])18 @@index([userId, createdAt])19}
Send notifications from anywhere in your application
1// src/lib/notifications.ts2import { prisma } from "@/lib/db";3import { pusherServer } from "@/lib/pusher/server";45interface CreateNotificationInput {6 userId: string;7 type: "info" | "success" | "warning" | "error";8 title: string;9 body?: string;10 link?: string;11}1213export async function createNotification(input: CreateNotificationInput) {14 // Save to database15 const notification = await prisma.notification.create({16 data: {17 userId: input.userId,18 type: input.type,19 title: input.title,20 body: input.body,21 link: input.link,22 },23 });2425 // Send real-time push26 await pusherServer.trigger(27 `user-${input.userId}`,28 "notification",29 {30 id: notification.id,31 type: notification.type,32 title: notification.title,33 body: notification.body,34 link: notification.link,35 createdAt: notification.createdAt.toISOString(),36 }37 );3839 return notification;40}4142// Usage examples43await createNotification({44 userId: user.id,45 type: "success",46 title: "Payment successful",47 body: "Your subscription has been activated",48 link: "/dashboard/billing",49});5051await createNotification({52 userId: user.id,53 type: "info",54 title: "New team member",55 body: "John Doe joined your organization",56});
The notification bell with real-time updates and dropdown
1"use client";23import { useState, useEffect } from "react";4import { Bell } from "lucide-react";5import { pusherClient } from "@/lib/pusher/client";6import {7 DropdownMenu,8 DropdownMenuContent,9 DropdownMenuTrigger,10} from "@/components/ui/dropdown-menu";1112interface Notification {13 id: string;14 type: string;15 title: string;16 body?: string;17 link?: string;18 read: boolean;19 createdAt: string;20}2122export function NotificationBell({ userId }: { userId: string }) {23 const [notifications, setNotifications] = useState<Notification[]>([]);24 const [unreadCount, setUnreadCount] = useState(0);2526 // Fetch existing notifications on mount27 useEffect(() => {28 fetch("/api/v1/notifications")29 .then((res) => res.json())30 .then((data) => {31 setNotifications(data.notifications);32 setUnreadCount(data.unreadCount);33 });34 }, []);3536 // Subscribe to real-time notifications37 useEffect(() => {38 const channel = pusherClient.subscribe(`user-${userId}`);3940 channel.bind("notification", (data: Notification) => {41 setNotifications((prev) => [data, ...prev]);42 setUnreadCount((prev) => prev + 1);43 });4445 return () => {46 channel.unbind_all();47 pusherClient.unsubscribe(`user-${userId}`);48 };49 }, [userId]);5051 const markAsRead = async (id: string) => {52 await fetch(`/api/v1/notifications/${id}/read`, { method: "POST" });53 setNotifications((prev) =>54 prev.map((n) => (n.id === id ? { ...n, read: true } : n))55 );56 setUnreadCount((prev) => Math.max(0, prev - 1));57 };5859 return (60 <DropdownMenu>61 <DropdownMenuTrigger asChild>62 <button className={cn('relative p-2 hover:bg-muted', mode.radius)}>63 <Bell className="h-5 w-5" />64 {unreadCount > 0 && (65 <span className={cn('absolute -top-1 -right-1 bg-destructive text-destructive-foreground text-xs h-5 w-5 flex items-center justify-center', mode.radius)}>66 {unreadCount > 9 ? "9+" : unreadCount}67 </span>68 )}69 </button>70 </DropdownMenuTrigger>71 {/* Dropdown content... */}72 </DropdownMenu>73 );74}
Notification management endpoints
1// GET /api/v1/notifications2export async function GET(req: Request) {3 const session = await auth();4 if (!session?.user) {5 return Response.json({ error: "Unauthorized" }, { status: 401 });6 }78 const notifications = await prisma.notification.findMany({9 where: { userId: session.user.id },10 orderBy: { createdAt: "desc" },11 take: 50,12 });1314 const unreadCount = await prisma.notification.count({15 where: { userId: session.user.id, read: false },16 });1718 return Response.json({ notifications, unreadCount });19}2021// POST /api/v1/notifications/:id/read22export async function POST(23 req: Request,24 { params }: { params: { id: string } }25) {26 const session = await auth();27 if (!session?.user) {28 return Response.json({ error: "Unauthorized" }, { status: 401 });29 }3031 await prisma.notification.update({32 where: { id: params.id, userId: session.user.id },33 data: { read: true, readAt: new Date() },34 });3536 return Response.json({ success: true });37}3839// POST /api/v1/notifications/read-all40export async function POST(req: Request) {41 const session = await auth();42 if (!session?.user) {43 return Response.json({ error: "Unauthorized" }, { status: 401 });44 }4546 await prisma.notification.updateMany({47 where: { userId: session.user.id, read: false },48 data: { read: true, readAt: new Date() },49 });5051 return Response.json({ success: true });52}