DEV Community

Cover image for ⛵Build a Collaborative App with Real-Time Comments & @Mentions Using Velt, Clerk Auth, Prisma & Radix UI🔑
Astrodevil
Astrodevil

Posted on

⛵Build a Collaborative App with Real-Time Comments & @Mentions Using Velt, Clerk Auth, Prisma & Radix UI🔑

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:

landing page

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 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.

Prisma

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")
}
Enter fullscreen mode Exit fullscreen mode

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.

Radix

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.

Clerk

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

Now, you have to install the necessary packages:

npm install @clerk/nextjs @veltdev/react @prisma/client zod next-themes
npm install prisma --save-dev
Enter fullscreen mode Exit fullscreen mode

Next, initialize Prisma in your project:

npx prisma init
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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)(.*)'],
};

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Important: You have to Enable In-App Notifications in your velt console dashboard

velt 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>;
}
Enter fullscreen mode Exit fullscreen mode

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 }
    );
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
);
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
}

Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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 });
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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}</>;
}
Enter fullscreen mode Exit fullscreen mode

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
       }
Enter fullscreen mode Exit fullscreen mode

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:

comment pin

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>
Enter fullscreen mode Exit fullscreen mode

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:

notifications

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
}
}
Enter fullscreen mode Exit fullscreen mode

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();
}
});
Enter fullscreen mode Exit fullscreen mode

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,
},
});
Enter fullscreen mode Exit fullscreen mode

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 }
);
}
}

Enter fullscreen mode Exit fullscreen mode

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>;
}
}

Enter fullscreen mode Exit fullscreen mode

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 });
}

Enter fullscreen mode Exit fullscreen mode

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");
});
});

Enter fullscreen mode Exit fullscreen mode

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);
});
});

Enter fullscreen mode Exit fullscreen mode

To run the tests, add these scripts to your package.json:

{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
axrisi profile image
Nikoloz Turazashvili (@axrisi)
  • 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:

    • Interactive Engagement: Helps build a community and enhances user engagement.
    • User Expectations: Users expect real-time updates and seamless collaboration in apps.
  • Tools and Technologies:

    • Framework: Next.js for building the app.
    • Database: Prisma for data management.
    • UI Components: Radix UI for accessible components.
    • Authentication: Clerk for user authentication management.
    • Real-Time Functionality: Velt for live collaboration capabilities.
  • Challenges of Building from Scratch:

    • Frontend:
      • Dynamic Comment Threads: Interface should update on new comments.
      • @Mentions Autocomplete: Implementing tagging with a dropdown for user suggestions.
      • Presence Indicators: Indicating online users and typing activity.
      • In-App Notifications: Alerting users when mentioned in comments.
    • Backend:
      • Data Storage: A reliable database structure for comments and user profiles.
      • Real-Time Data Sync: Instant updates for comments and notifications.
      • Secure API Endpoints: Handling post comments and mentions securely.
      • Input Validation: Preventing spam and ensuring data integrity.
  • Using the Velt SDK:

    • Simplified Integration: Velt offers components for real-time features, reducing coding complexity.
    • Automatic Data Handling: Manages client-side logic and real-time syncing behind the scenes.
    • Security and Scaling: Inherited security features prevent auth loopholes and performance issues.
  • Building a Commenting System:

    • Authentication Setup: Utilizing Clerk for secure user management.
    • Database Model: Setting up Comment and Mention models in Prisma.
    • API Integration: Creating secure API routes for comments with effective error handling.
  • User Interaction Features:

    • @Mentions Capability: Automatically showing users to mention when @ is typed.
    • Real-Time Notifications: Handling notifications for mentions effectively.
    • Testing: Importance of error handling and unit tests for components and API routes.
  • Future Enhancements:

    • Consider adding comment moderation, pagination, and caching for performance improvements.

made with love by axrisi
axrisi.com

Collapse
 
astrodevil profile image
Astrodevil

Is this a summarize tool or extension?

Collapse
 
axrisi profile image
Nikoloz Turazashvili (@axrisi)

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.

Thread Thread
 
axrisi profile image
Nikoloz Turazashvili (@axrisi)

I just launched it today on producthunt :)
producthunt.com/products/axrisi-ai...

Thread Thread
 
astrodevil profile image
Astrodevil

cool, will support!

Collapse
 
arindam_1729 profile image
Arindam Majumder

Wow, great use case!

Collapse
 
astrodevil profile image
Astrodevil

Thanks Arindam!

Some comments may only be visible to logged-in visitors. Sign in to view all comments.