Interactive features like live comments and @mentions have become essential in modern web applications. Users now expect real-time updates and seamless collaboration, making these tools crucial for keeping people engaged and for making your app stand out.
But real-time commenting with mentions doesn’t just create a more interactive experience, it helps build a genuine sense of community in your user base. When people can talk about content, share feedback and help one another on your platform, they’ll never want to leave.
Don’t believe us? Ask Reddit!
Simply put, adding real-time commenting is a no-brainer if you want to boost user engagement and loyalty.
In this article, we’ll’ll show you how to build a real-time commenting system using Next.js, Prisma, Radix UI, Clerk Auth and Velt for the live collaboration features.
You’ll learn:
- How to streamline the authentication process
- How to handle data flow
- How to provide updates in real-time.
By the end, you’ll have a solid framework for a modern collaborative platform that scales effortlessly and delivers an engaging experience to your users.
Final app landing page will look like this:
But first, let’s look at the challenges you’d likely face when building these features from scratch.
Challenges of Building Real‑Time Comments with @Mentions from Scratch
Building a real-time commenting system with mentions from scratch can be pretty complex. You’ll need an intuitive frontend, and a reliable backend that can process comments in real time. So it’s not just about writing code; it’s also about the time and development manpower you’ll invest.
While it’s still doable, you’ll likely face the following major challenges.
Frontend:
Here’s a breakdown of what you’ll face:
- Interactive Comment Threads: You’ll need a dynamic interface where comments (and replies) update as soon as they’re posted.
- @Mentions Autocomplete: Tagging another user might sound simple, but it requires detecting an “@” keystroke and instantly showing a dropdown of user suggestions.
- Presence Indicators: If you want users to feel they’re part of a live discussion truly, you’ll need to show who else is online or typing.
- In-App Notifications: When someone mentions a user or replies to their comment, that user should know immediately.
Getting a headache yet? Hang on, there’s more!
Backend:
Building the backend for a real-time commenting system can be just as challenging as creating the frontend. You’ll need:
- Data Storage and Retrieval: A solid database design that can store comments, user profiles for mentions and the threads or reply structures linking them all together.
- Real-Time Data Sync: To achieve instant updates, your backend must push new comments and notifications to clients in real time.
- API Endpoints for Comments and Mentions: You’ll need to build secure API routes to handle creating and fetching comments. For instance, a “post comment” API should accept the comment text (and maybe a thread ID if it’s a reply) and a list of mentioned user IDs.
- Authentication and Authorization: Security is critical, only authenticated users should be allowed to post comments or view certain information. The backend needs to verify the user’s identity (e.g. via a token or session) on each request.
- Input Validation and Spam Control: Accepting user-generated content means you have to be vigilant about input. To prevent spam or malicious data, you should validate incoming comment data.
- Notifications and Mentions Logic: When a comment includes mentions, the backend has to create notification entries (or send emails/real-time alerts) to those mentioned users.
As you can see, it’s already quite a lot. And considering the points above, creating a custom real-time commenting system will require substantial resources in terms of time and dev effort, expertise and ongoing maintenance. If still go ahead to do it yourself, you’ll still run into issues scaling and enhancing the system.
My advice? No, don’t create everything from scratch. Not when you can easily use Velt
Introducing the Velt SDK
Velt is a developer toolkit that brings real-time collaboration features to your app without all the heavy lifting. It’s like a plug-and-play solution for the kind of live commenting, presence indicators, and in-app notifications you see in apps like Figma or Notion, but bundled into a React library.
Instead of wrestling with custom WebSockets, real-time protocols and complicated UI, you can simply drop Velt’s components into your code. Whether you’re adding text comments, mentions, reactions or audio notes, Velt handles the entire client-side logic and real-time syncing behind the scenes, so you don’t have to do manually.
Even on the backend, Velt significantly simplifies things. Once you connect your app to Velt via an API key, its service ensures new comments are instantly broadcasted to everyone watching and that mention notifications go out in real time. You can still store data in your database for record-keeping or tie it into any existing user authentication, Velt plays nicely with all that.
The best part? You also inherit Velt’s built-in security and scaling features, making you less likely to run into auth loopholes or performance bottlenecks. In short, Velt does the heavy lifting, so you can get a collaborative app up and running quickly, without sacrificing reliability or user experience.
Now, let’s get into the fun part and show you how to build a commenting and mentions system with Velt🎉
Why We Chose The Tools We’re Using
Let's start with Prisma. In our commenting system, we need to handle complex relationships between comments, mentions and users. We chose Prisma for its strong type-safety, easy schema management, and efficient data handling.
Its type-safe database client catches errors at compile time, boosting development speed and ensuring consistent data handling.
model Comment {
id String @id @default(cuid())
content String
authorId String // Clerk user ID
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
parentId String? // For nested comments
parent Comment? @relation("CommentToComment", fields: [parentId], references: [id])
replies Comment[] @relation("CommentToComment")
mentions Mention[] @relation("CommentMentions")
}
Radix UI is used for the UI of our app, because it provides headless, unstyled components that automatically adhere to accessibility best practices. This ensures elements like dropdown for @mentions, modals for notifications and tooltips are accessible by default, while allowing full design flexibility via CSS or utility classes.
Clerk handles our authentication needs. It provides a seamless auth experience and integrates perfectly with Next.js. We use it to manage user sessions, profiles, and secure our API routes.
Here's how we protect our comment endpoints:
import { auth } from "@clerk/nextjs/server";
export async function POST(req: Request) {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Handle comment creation
}
You can also use Upstash Redis for caching, but that’s optional.
At the end of this implementation, your .env.example
file in the root directory of your project should have following variables:
# Clerk Authentication
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key
CLERK_SECRET_KEY=your_clerk_secret_key
# Velt Real-time Collaboration
NEXT_PUBLIC_VELT_API_KEY=your_velt_api_key
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/your_database_name"
# Rate Limiting
RATE_LIMIT_MAX=60
RATE_LIMIT_WINDOW_MS=60000
Prerequisites:
- For this tutorial, you should have Node.js installed and a basic familiarity with Next.js.
- You will also need a free account for Clerk (to get API keys for auth) and for Velt (to get an API key for the collaboration features).
Setting Up Your Project
First things first, let’s set up our Next.js project and install everything we need. We’ll be using Clerk for authentication, Velt for real-time collaboration and some theming support to keep things looking sharp.
Open your terminal and run the following commands to create a TypeScript-based Next.js application with all the required packages:
`npx create-next-app@latest comments-app --typescript`
`cd comments-app`
Now, you have to install the necessary packages:
npm install @clerk/nextjs @veltdev/react @prisma/client zod next-themes
npm install prisma --save-dev
Next, initialize Prisma in your project:
npx prisma init
It’s time to set up our database schema. Create the following models in your prisma/schema.prisma
file:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Comment {
id String @id @default(cuid())
content String
authorId String // Clerk user ID
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
parentId String?
parent Comment? @relation("CommentToComment", fields: [parentId], references: [id], onDelete: Cascade)
replies Comment[] @relation("CommentToComment")
mentions Mention[] @relation("CommentMentions")
@@index([authorId])
@@index([parentId])
@@index([createdAt])
}
model Mention {
id String @id @default(cuid())
userId String // Clerk user ID of the mentioned user
commentId String
createdAt DateTime @default(now())
comment Comment @relation("CommentMentions", fields: [commentId], references: [id], onDelete: Cascade)
notification Notification? @relation("MentionNotification")
@@index([userId])
@@index([commentId])
@@unique([userId, commentId])
}
Configuring Clerk Auth
Clerk will handle user accounts and sessions in our app. To set it up, you need to provide your Clerk credentials and initialize Clerk’s middleware in Next.js. Create a .env.local file in your project root and add your Clerk credentials:
`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key`
`CLERK_SECRET_KEY=your_secret_key`
Since Clerk handles authentication, we’ll need to set up some middleware to manage user sessions properly. Just head over to your project’s root directory and either create or update the middleware.ts
file. This will ensure that authentication works seamlessly across your app, keeping your comment system secure and personalized for each user.
import { authMiddleware } from "@clerk/nextjs";
export default authMiddleware({
// Public routes that don't require authentication
publicRoutes: ["/", "/api/public"]
});
export const config = {
matcher: ['/((?!.+\\\\.[\\\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
};
Configuring Velt
Now, we’ll connect the Velt SDK. Like Clerk, Velt requires an API key for its functionalities. You need to open an account Velt, then go to your Velt account/dashboard to obtain an API key, then add it to the same .env.local
file:
NEXT_PUBLIC_VELT_API_KEY=your_velt_api_key
Important: You have to Enable In-App Notifications in your velt console dashboard
Next, we’ll configure Providers.tsx
components to link Clerk’s authentication system with Velt’s real-time collaboration features. By wrapping our app with both Clerk and Velt providers, we ensure authentication context and collaboration tools are accessible throughout the application. Finally, we’ll pass the authenticated user information to Velt to enable user-specific real-time interactions.
"use client";
import { ClerkProvider, useUser } from "@clerk/nextjs";
import { VeltProvider } from "@veltdev/react";
function VeltProviderWithAuth({ children }: { children: React.ReactNode }) {
const { user, isSignedIn } = useUser();
const veltUser = isSignedIn && user
? { userId: user.id, name: user.fullName || "Anonymous", /* ... */ }
: { userId: "anonymous", name: "Anonymous", /* ... */ };
return (
<VeltProvider
apiKey={process.env.NEXT_PUBLIC_VELT_API_KEY}
user={veltUser} // Pass user directly to VeltProvider
>
{children}
</VeltProvider>
);
}
// Then in the child component:
function CommentSection() {
const { client } = useVeltClient(); // This is safe because it's inside VeltProvider
const { user } = useUser();
useEffect(() => {
if (client && user) {
client.identify({
userId: user.id,
name: user.fullName || "Anonymous",
// ... other user data
});
}
}, [client, user]);
return <div>...</div>;
}
Here, we set up a combined provider that makes sure Clerk and Velt work together. Notice how we create a Velt user from Clerk’s data and configure the real-time commenting features. Pretty neat right?
Securing Your Routes
To protect your comment-related endpoints, create an API route with Clerk's protection:
import { auth } from "@clerk/nextjs";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { userId } = auth();
// Check if user is authenticated
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
// Your comment handling logic here
const body = await request.json();
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json(
{ error: "Failed to process request" },
{ status: 500 }
);
}
}
Here we ensure that only logged-in users can submit comments, helping to keep your application secure.
Let’s complete the setup by wrapping your application with the Providers component. This ensures that both Clerk’s authentication and Velt’s real-time features are available throughout your app.
Open your layout.tsx
file (or _app.tsx
if using the Pages Router) and update it like this:
import { Providers } from "@/components/Providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Real-Time Comments Page
Now it’s time to see the live commenting feature in action, so we’ll build the page where users can view and post comments in real time.
In a Next.js app using the App Router, you’d create the /comments
page by adding a file at app/comments/page.tsx
. If you’re using the older Pages Router, you’d instead place the file at pages/comments.tsx
.
In this page component, we will use Velt’s React components to set up the commenting UI:
"use client";
import { VeltComments, VeltCommentTool } from "@veltdev/react";
import { useUser } from "@clerk/nextjs";
export default function CommentsPage() {
const { isLoaded, isSignedIn } = useUser();
const router = useRouter();
useEffect(() => {
if (isLoaded && !isSignedIn) { router.push("/sign-in?redirect_url=/comments");
}
}, [isLoaded, isSignedIn, router]);
useSetDocument("comments-page", {
documentName: "Comments Page",
documentDescription: "Page for managing and viewing comments",
metadata: {
type: "comments",
status: "active",
},
});
if (!isSignedIn) return null;
return (
<main>
<h1>Comments</h1>
<VeltComments id="comments-page" />
<VeltCommentTool id="comments-page" />
</main>
);
}
Before the page is rendered, it checks if the user is signed in and redirects them to the sign-up page if needed. Once the user is authenticated, various Velt components enable live updates and interactions on the comments page.
At this point, the frontend is essentially done. If you run the Next.js app, the /comments
page would show the comment box and (initially empty) comment thread. The presence indicator and notification bell would also appear, but they won’t do much until someone is online or a notification is triggered. If you try to submit a comment now, nothing will happen just yet – because we haven’t implemented the backend logic to handle posting comments or retrieving any existing comments/notifications.
Backend for Comments and @Mentions
We need an API route that will accept new comments (and also handle mentions within those comments). In a Next.js app using the App Router, you’d create the /comments
page by adding a file at app/comments/page.tsx
. If you’re using the older Pages Router, you’d instead place the file at pages/comments.tsx
.
Here’s an example implementation using Next.js API route handlers, Clerk for auth, and Prisma (an ORM) for database operations:
import { auth } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { createCommentSchema } from "@/lib/validations";
export async function POST(req: Request) {
const { userId } = await auth();
if (!userId)
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const body = createCommentSchema.parse(await req.json());
const comment = await prisma.comment.create({
data: { content: body.content, authorId: userId, parentId: body.parentId },
});
// Optionally handle @mentions...
return NextResponse.json(comment);
}
This is how our API handles creating a new comment while keeping things secure and efficient. It includes rate limiting to prevent spam, validates user input with Zod, and ensures that mentions trigger the right notifications. Plus, it smoothly manages the creation of mention records so users get notified when they’re tagged in a conversation.
Implementing @mentions
Thanks to Velt, much of the mentions functionality is automatically handled on the frontend. We configured userSuggestions
in the Velt provider to supply a list of mentionable users. When a user types @
, Velt automatically displays a dropdown list of users they can mention.
To make this work, we first need to pass the right user data from Clerk to Velt. When a user signs in through Clerk, the authentication state is managed by Clerk. The VeltProviderWithAuth component then detects the signed-in state and creates a corresponding Velt user. The useIdentify hook from Velt is used to tell Velt who the current user is, while the LayoutContent component provides additional user context to Velt, including organization information.
Velt then uses this information to power its real-time features like presence indicators, mentions, and notifications, making the @mention feature seamless and intuitive for users.
function VeltProviderWithAuth({ children }: { children: ReactNode }) {
const { user, isLoaded, isSignedIn } = useUser();
const apiKey = process.env.NEXT_PUBLIC_VELT_API_KEY;
// Create a Velt user from Clerk's user data
const veltUser =
isSignedIn && user
? {
userId: user.id,
name: user.fullName || user.username || "Anonymous",
email: user.emailAddresses[0]?.emailAddress || "",
photoUrl: user.imageUrl || "",
organizationId: "default-org",
}
: {
userId: "anonymous",
name: "Anonymous",
email: "",
photoUrl: "",
organizationId: "default-org",
};
const veltConfig = {
apiKey,
debug: true,
user: veltUser,
organizationId: "default-org",
// ... other configuration options
};
return <VeltProvider {...veltConfig}>{children}</VeltProvider>;
}
The user data comes directly from your Clerk authentication, ensuring that only real, authenticated users can be mentioned.
Next, we can enable contact list management with @mentions by providing Velt with a list of users who can be mentioned. This involves two key components:
- An API endpoint that returns users in Velt's expected format:
// src/app/api/users/route.ts
export async function GET() {
try {
const { userId } = await auth();
if (!userId) return NextResponse.json({ users: [], groups: [] });
const client = await clerkClient();
const users = await client.users.getUserList({
limit: 100,
orderBy: '-created_at'
});
const veltUsers = users.data.map(user => ({
userId: user.id,
name: `${user.firstName || ""} ${user.lastName || ""}`.trim() || "Anonymous",
email: user.emailAddresses[0]?.emailAddress || "",
photoUrl: user.imageUrl || "",
groups: ["organization"]
}));
return NextResponse.json({
users: veltUsers,
groups: [{
id: "organization",
name: "Organization",
description: "All users in the organization"
}]
});
} catch (error) {
return NextResponse.json({ users: [], groups: [] }, { status: 500 });
}
}
- A component that manages the contact list using Velt's utilities:
function VeltUserSetup({ children }: { children: ReactNode }) {
const { user, isSignedIn } = useUser();
// Get access to Velt's contact utilities
const contactElement = useContactUtils();
const [allUsers, setAllUsers] = useState<Array<{
userId: string;
name: string;
email: string;
photoUrl: string;
}>>([]);
// Fetch all users from our API
useEffect(() => {
async function fetchUsers() {
if (!isSignedIn) return;
const response = await fetch("/api/users");
const users = await response.json();
setAllUsers(users);
}
fetchUsers();
}, [isSignedIn]);
// Update Velt's contact list with our users
useEffect(() => {
if (contactElement && allUsers.length > 0) {
contactElement.updateContactList(allUsers, { merge: false });
}
}, [contactElement, allUsers]);
return <>{children}</>;
}
This @mention system works through a combination of our custom logic and Velt’s built-in functionality.
At the core of this setup is the useContactUtils
hook, which provides access to contact-related utilities we need to manage mentionable users, mainly through updateContactList
, which we use to specify the users that can be mentioned in a given context. The updateContactList
method takes two parameters: a list of users formatted according to Velt’s requirements, and an options object that allows us to configure how the update behaves.
{
merge: boolean, // Whether to merge with existing contacts
scope?: string // Optional: Limit contacts to specific document/location
}
So when someone types “@” in a comment, Velt instantly looks up the list of users we’ve shared using updateContactList
and pops up a dropdown with those names. All the user has to do is pick someone from the list, and Velt automatically drops their name into the comment as a mention—no extra steps needed. It feels natural and effortless. Plus, because the list stays synced with your Clerk user base, any new users are added automatically, so everyone’s always up to date and ready to be mentioned.
Example of a comment pin when someone is mentioned in a comment:
Real-Time Presence and Notifications
Real-time interactions make your app dynamic. This guide shows how Velt provides a live presence indicator and notification bell.
For example, the notifications panel is simply added with:
<div className="fixed top-4 right-24 z-50">
<VeltNotificationsTool />
</div>
This component automatically displays any new notifications such as mentions, ensuring users stay informed about recent activities.
We already added the <VeltPresence>
and <VeltNotificationsTool>
components on the frontend. The presence component works automatically through Velt’s real-time service, whenever a user visits the page, and the useSetDocument("comments-page")
runs, Velt’s service knows this user is present in that document and will inform others via the presence component. So no custom backend code was needed for presence beyond the identification we did.
The notifications panel should display @mention notifications like this:
The notification system is backed by a dedicated API route that handles fetching notifications:
export async function GET(req: Request) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(req.url);
const parsed = paginationSchema.parse({
page: searchParams.get("page"),
limit: searchParams.get("limit"),
});
const skip = (parsed.page - 1) * parsed.limit;
// Fetch notifications with mention details
const [notifications, total] = await Promise.all([
prisma.notification.findMany({
where: {
userId,
},
include: {
mention: {
include: {
comment: true,
},
},
},
orderBy: {
createdAt: "desc",
},
skip,
take: parsed.limit,
}),
prisma.notification.count({
where: {
userId,
},
}),
]);
return NextResponse.json({
notifications,
total,
pages: Math.ceil(total / parsed.limit),
});
} catch (error) {
// Error handling
}
}
At this point, we have:
- Real-time commenting UI
- The ability to post comments with mention
- Real-time presence (handled by Velt)
- Notifications for mentions (creating and fetching)
Security Best Practices
Let’s summarize the security best practices we have applied so far to be clear:
Authentication Protection
All sensitive routes we use (the comments page and related APIs) check for a valid authenticated user. Unauthenticated requests get a 401 or redirect to login, as this guarantees that only legitimate users can post or view comments. Here’s how we did it:
import { clerkMiddleware } from "@clerk/nextjs/server";
import type { NextRequest } from "next/server";
export default clerkMiddleware((auth, req: NextRequest) => {
*// Allow Velt API routes to bypass authentication*
const url = req.nextUrl.pathname;
if (url.startsWith("/api/velt")) {
return NextResponse.next();
}
});
This middleware function uses Clerk to enforce authentication on all requests by default. However, if a request’s URL path starts with /api/velt
, it skips the authentication check and allows the request to proceed without requiring a logged-in user. Essentially, it locks down your entire app using Clerk’s auth while still providing a public “hole” for any endpoints under /api/velt
, which is convenient if you want some Velt-specific API routes to be accessible without logging in.
Input Validation and Spam Prevention
We used schema validation (Zod
) for incoming data to avoid malicious input. We also implemented rate limiting on comment submissions to prevent spamming. The rate limiting is applied to the comments API endpoint, which helps prevent spam and abuse by limiting how frequently a single user can post comments.
const rateLimitResult = await limiter.check(userId);
if (rateLimitResult.status === 429) {
return rateLimitResult;
}
const json = await req.json();
const body = createCommentSchema.parse(json);
const comment = await prisma.comment.create({
data: {
content: body.content,
authorId: userId,
parentId: body.parentId,
},
include: {
mentions: true,
},
});
This helps maintain the integrity of the system and user experience, and it’s set at 60 requests every 60 seconds, effectively preventing spam and brute forcing overwhelming the app.
Secure User Identification
We always identify the user through Clerk and pass that identity to Velt (useIdentify
). This means Velt’s real-time events and presence are tied to actual user accounts. There’s no chance for a user to impersonate someone else in the real-time system, because Clerk provides a verified userId
that we give to Velt. All comment actions on the backend also rely on userId
from Clerk’s JWT – ensuring, for example, a user cannot post a comment as another user by tinkering with the client, since our auth()
will catch the true identity on the server.
Handling Safe Responses
Our API catches errors and returns generic messages for unexpected issues. We specifically catch validation errors to inform the client what went wrong, but for other errors we avoid leaking details (just returning “Internal Server Error”). We also log errors on the server for debugging. This approach prevents exposing stack traces or sensitive info to the client, which is a good security practice.
export async function GET(req: Request) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
*// ... handle the request ...*
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.errors }, { status: 400 });
}
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
);
}
}
Here, we use a Next.js serverless route that verifies that the request comes from a signed-in user by checking userId
. If there’s no user, it immediately returns a 401 (Unauthorized) response. If the user is valid, the code proceeds to handle the request. If any validation fails (e.g., via Zod), it sends back a 400 (Bad Request) with specific error details. For any other unhandled problem, the function responds with a 500 (Internal Server Error).
Error Handling and Testing
Finally, let's implement proper error handling and set up testing for our comments system.
Error Management
First, let's add error handling to our Velt initialization:
export function LayoutContent({ children }: { children: React.ReactNode }) {
const { userId, isLoaded, isSignedIn } = useAuth();
const { user } = useUser();
const { client } = useVeltClient();
const [veltError, setVeltError] = useState<string | null>(null);
useEffect(() => {
async function initializeVelt() {
if (!client || !isLoaded || !isSignedIn || !userId || !user) return;
try {
await client.identify({
userId,
name: user.fullName || "",
email: user.primaryEmailAddress?.emailAddress || "",
organizationId: organization?.id || "default-org",
});
} catch (error) {
console.error("Error initializing Velt:", error);
setVeltError(
"Failed to initialize real-time features. Please check if you have any content blockers enabled."
);
}
}
initializeVelt();
}, [client, isLoaded, isSignedIn, userId, user, organization]);
if (veltError) {
return <div className="text-red-500">{veltError}</div>;
}
}
This component simply checks if the user is authenticated and the Velt client is ready, then it calls client.identify
to register the user with Velt’s real-time system. If any errors occur, it logs them and displays a short warning message to the user.
For our API endpoints, we implement comprehensive error handling and rate limiting:
try {
// Rate limiting
const rateLimitResult = await limiter.check(userId);
if (rateLimitResult.status === 429) {
return rateLimitResult;
}
// Main logic...
} catch (error) {
if (error instanceof z.ZodError)
return NextResponse.json({ error: error.errors }, { status: 400 });
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
First, we apply a rate limit to block spammers and excessive requests, and once that’s clear, we validate the request data with Zod to ensure everything is in the right format.
If something goes wrong—like a database error or invalid data—we catch it and return an error response so users get a clear message about what happened.
Testing
Let's set up tests for our comment components and API endpoints. First, create a test for the Comment component:
import "@testing-library/jest-dom";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useAuth } from "@clerk/nextjs";
import { Comment } from "@/components/Comment";
// Mock Clerk's useAuth hook
jest.mock("@clerk/nextjs", () => ({
useAuth: jest.fn(),
}));
describe("Comment Component", () => {
const mockComment = {
id: "1",
content: "Test comment",
authorId: "user_123",
createdAt: new Date().toISOString(),
mentions: [],
};
beforeEach(() => {
(useAuth as jest.Mock).mockReturnValue({
userId: "user_123",
isLoaded: true,
isSignedIn: true,
});
});
it("renders comment content", () => {
render(<Comment {...mockComment} />);
expect(screen.getByText("Test comment")).toBeInTheDocument();
});
it("shows edit options for comment author", () => {
render(<Comment {...mockComment} />);
expect(screen.getByRole("button", { name: /edit/i })).toBeInTheDocument();
});
it("handles edit submission", async () => {
const onEdit = jest.fn();
render(<Comment {...mockComment} onEdit={onEdit} />);
const editButton = screen.getByRole("button", { name: /edit/i });
await userEvent.click(editButton);
const input = screen.getByRole("textbox");
await userEvent.type(input, " updated");
const submitButton = screen.getByRole("button", { name: /save/i });
await userEvent.click(submitButton);
expect(onEdit).toHaveBeenCalledWith("1", "Test comment updated");
});
});
Next, we mock Clerk’s useAuth
hook to simulate a signed-in user with the same ID as the comment’s author. Then, we verify that the component shows the comment text on screen and displays the edit button for the author. When we click “Edit,” the test types in updated text and hits “Save,” and we check that it calls the onEdit
callback with the right arguments. This way, you can be confident that the editing workflow works end-to-end for the user who posted the comment.
Next, let's test our API endpoints:
// Mock Clerk auth
jest.mock("@clerk/nextjs/server", () => ({
auth: jest.fn(),
}));
// Mock Prisma
jest.mock("@/lib/db", () => ({
prisma: {
comment: {
create: jest.fn(),
},
mention: {
createMany: jest.fn(),
},
notification: {
createMany: jest.fn(),
},
},
}));
describe('Comments API', () => {
beforeEach(() => {
jest.clearAllMocks();
(auth as jest.Mock).mockReturnValue({ userId: 'test_user' });
});
it('creates a comment successfully', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
content: 'Test comment',
parentId: null,
mentionedUserIds: [],
},
});
(prisma.comment.create as jest.Mock).mockResolvedValue({
id: '1',
content: 'Test comment',
authorId: 'test_user',
mentions: [],
});
await POST(req);
expect(res._getStatusCode()).toBe(200);
expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({
content: 'Test comment',
})
);
});
it('handles unauthorized requests', async () => {
(auth as jest.Mock).mockReturnValue({ userId: null });
const { req, res } = createMocks({
method: 'POST',
body: {
content: 'Test comment',
},
});
await POST(req);
expect(res._getStatusCode()).toBe(401);
});
it('validates comment data', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
content: '', // Empty content should fail validation
},
});
await POST(req);
expect(res._getStatusCode()).toBe(400);
});
});
To run the tests, add these scripts to your package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
With the extra error handling and tests in place, our comments/notifications system is now much more reliable and user-friendly. If something goes wrong—like an invalid request or someone trying to post without being signed in—the system catches it right away and sends back a clear error message.
The automated tests also give you peace of mind that each part of the commenting workflow, including creating new comments and mentions, keeps working as you make changes. Ultimately, it means fewer surprises in production and a better experience for everyone.
Woah 🎉 The app is now fully configured, providing a solid foundation to scale into a full SaaS product with real-time collaborative features!
Demo
Conclusion and Next Steps
If you’ve followed this article so far, great job on putting together a commenting system that combines secure authentication with real-time collaboration using Next.js, Prisma, Radix UI, Clerk Auth and Velt. While this is a simple demo, you can build upon it for your projects using these tools.
With a solid foundation in place, we can further enhance the commenting system as needed. For example, we could add support for deeper comment threading (replies to replies), or moderation features like editing and deleting comments. We might implement advanced notification options (such as email notifications for mentions, or the ability to mark notifications as read).
On the performance side, as the number of comments grows, we should look into caching and pagination for loading comments. We already included pagination parameters for notifications; similarly, if a page accumulates thousands of comments, we’d want to load them in chunks and maybe cache recent comments for quick retrieval.
Resources you can refer:
Thankyou for reading! If you found this article useful, share it with your peers and community.
If You ❤️ My Content! Connect Me on Twitter
Check SaaS Tools I Use 👉🏼Access here!
I am open to collaborating on Blog Articles and Guest Posts🫱🏼🫲🏼 📅Contact Here
Top comments (7)
Overview: The text discusses the importance of real-time commenting features in modern web applications and provides a guide to building such a system using specific tools.
Importance of Real-Time Comments:
Tools and Technologies:
Challenges of Building from Scratch:
Using the Velt SDK:
Building a Commenting System:
User Interaction Features:
Future Enhancements:
made with love by axrisi

Is this a summarize tool or extension?
well, kinda both :)
It is chrome extension where you can summarize, bulletize, ELI5, continue writing and correct grammar.
And any text that you process you can save as Notes that you can access later. as well as resource gets saved.
I just launched it today on producthunt :)
producthunt.com/products/axrisi-ai...
cool, will support!
Wow, great use case!
Thanks Arindam!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.