# Implementing a Comments System with Nested Replies and Voting in prompts.chat

> Learn to build a nested comment system with voting in prompts.chat. Explore Prisma, Next.js API routes, and recursive React components for a robust threaded discussion feature.

- Repository: [Fatih Kadir Akın/prompts.chat](https://github.com/f/prompts.chat)
- Tags: how-to-guide
- Published: 2026-04-02

---

**The prompts.chat codebase delivers a production-ready, threaded comment system using Prisma for hierarchical data storage, Next.js API routes for vote transactions, and recursive React components for rendering nested replies up to five levels deep.**

This implementation enables users to engage with AI prompts through a fully type-safe, full-stack comments architecture. The system supports reading, writing, replying, voting, flagging, and soft-deleting comments while maintaining real-time UI synchronization and shadow-ban moderation capabilities.

## Architecture Overview

The comments feature spans three distinct layers that work together to deliver a seamless user experience.

- **Database Layer**: Stores hierarchical relationships, cached vote scores, and moderation flags in the `Comment` and `CommentVote` models defined in **[prisma/schema.prisma](https://github.com/f/prompts.chat/blob/main/prisma/schema.prisma#L24-L60)**.
- **API Layer**: Exposes REST-style endpoints for fetching comment trees, creating posts, casting votes, and moderation actions. Core handlers reside in **[src/app/api/prompts/[id]/comments/route.ts](https://github.com/f/prompts.chat/blob/main/src/app/api/prompts/%5Bid%5D/comments/route.ts)** and **[src/app/api/prompts/[id]/comments/[commentId]/vote/route.ts](https://github.com/f/prompts.chat/blob/main/src/app/api/prompts/%5Bid%5D/comments/%5BcommentId%5D/vote/route.ts)**.
- **Client Layer**: React Server Components fetch initial data while interactive client components handle vote toggling, reply forms, and recursive tree rendering via **[src/components/comments/comment-section.tsx](https://github.com/f/prompts.chat/blob/main/src/components/comments/comment-section.tsx)** and **[src/components/comments/comment-item.tsx](https://github.com/f/prompts.chat/blob/main/src/components/comments/comment-item.tsx)**.

## Database Schema for Hierarchical Comments

The Prisma schema establishes a self-referential relationship enabling infinite nesting (limited to five levels in the UI). The `Comment` model uses a nullable `parentId` field to reference parent comments, creating the `CommentReplies` relation.

Key fields include:
- `parentId`: Nullable foreign key establishing the comment thread hierarchy.
- `score`: Cached integer sum of all vote values, updated transactionally to avoid expensive aggregation queries.
- `flagged`: Boolean enabling shadow-ban moderation; flagged comments remain visible to admins and authors but hide from general users.
- `deletedAt`: Timestamp for soft deletion, preserving thread integrity while removing content.

The `CommentVote` model captures user votes with a composite unique index on `userId_commentId` to prevent duplicate voting, as implemented in the vote transaction logic.

## API Endpoints for CRUD and Voting

The API surface provides granular endpoints for specific actions, all protected by the `auth()` helper from **[src/lib/auth.ts]**.

**Fetching Comments**
The `GET` handler in **[route.ts](https://github.com/f/prompts.chat/blob/main/src/app/api/prompts/%5Bid%5D/comments/route.ts)** returns all comments for a prompt, including the current user's vote status and filtering logic for shadow-banned content (lines 80-90). The endpoint checks prompt privacy settings before returning data, ensuring private prompts remain accessible only to their authors.

**Creating Comments**
The `POST` handler validates authentication, enforces feature flags, and creates either top-level comments or replies by accepting an optional `parentId` in the request body (lines 21-70).

**Voting Mechanism**
The dedicated vote route at **[vote/route.ts](https://github.com/f/prompts.chat/blob/main/src/app/api/prompts/%5Bid%5D/comments/%5BcommentId%5D/vote/route.ts)** handles `POST` requests with a `value` of `1` (upvote) or `-1` (downvote). The server deduplicates votes using the unique database index and toggles off identical votes when clicked twice (lines 13-46). After each vote, the `score` field updates to reflect the new total.

## React Components for Threaded UI

The client-side implementation uses a recursive component architecture to render nested threads without blocking the initial page load.

**CommentSection**
The **[comment-section.tsx](https://github.com/f/prompts.chat/blob/main/src/components/comments/comment-section.tsx)** container fetches the flat comment array from the API, sorts top-level entries by score descending then creation date ascending (lines 94-98), and maps each to a `CommentItem`. It also manages the `handleCommentDeleted` callback (lines 72-83) which recursively removes deleted comments and their descendants from the local state to maintain UI consistency with the soft-delete backend.

**CommentItem**
Each **[comment-item.tsx](https://github.com/f/prompts.chat/blob/main/src/components/comments/comment-item.tsx)** renders the author avatar, timestamp, auto-linked content, vote controls, and action dropdowns. It recursively renders child comments by filtering the flat array (`allComments.filter(c => c.parentId === comment.id)`) up to the fifth nesting level. The component includes the `handleFlag` function (lines 85-102) for admin moderation.

**CommentForm**
The **[comment-form.tsx](https://github.com/f/prompts.chat/blob/main/src/components/comments/comment-form.tsx)** component provides the textarea and submit button used for both top-level comments and inline replies. It displays a login modal when unauthenticated users attempt to submit, ensuring proper session handling via `auth()`.

## Permission and Moderation Logic

The system enforces granular permissions at both the API and UI layers.

- **Viewing**: Private prompt checks occur before fetching comments, ensuring data isolation.
- **Posting**: Authentication required; anonymous users see a login modal.
- **Voting**: Any authenticated user can vote once per comment; clicking the same vote direction toggles it off.
- **Flagging**: Restricted to admins (`session?.user?.role === "ADMIN"`). The flag action toggles the `flagged` boolean, immediately shadow-banning the comment from public view while preserving it for moderation review.
- **Deletion**: Authors and admins may delete comments. The backend performs a soft delete by setting `deletedAt`, while the frontend recursively purges the comment tree from local React state.

## Summary

- **Hierarchical Storage**: The `Comment` model uses `parentId` with a self-referential relation to enable nested threading, limited to five levels in the UI.
- **Cached Scoring**: The `score` column stores pre-calculated vote totals, updated transactionally in the vote route to ensure read performance.
- **Shadow Banning**: The `flagged` boolean enables moderation without breaking thread context; admins see flagged content while regular users do not.
- **Soft Deletes**: Deletion sets a `deletedAt` timestamp rather than removing rows, preserving reply chain integrity.
- **Type-Safe API**: Next.js App Router routes in `src/app/api/prompts/[id]/comments/` provide type-safe handlers for listing, creating, voting, and flagging comments.
- **Recursive UI**: `CommentItem` recursively renders child comments from a flat API response, avoiding complex database joins while maintaining thread visualization.

## Frequently Asked Questions

### How does prompts.chat handle nested reply depth?

The system stores hierarchical data using a `parentId` foreign key in the `Comment` model, allowing theoretically infinite nesting. However, the UI enforces a practical limit of five nesting levels in **[comment-item.tsx](https://github.com/f/prompts.chat/blob/main/src/components/comments/comment-item.tsx)** to prevent excessive indentation and maintain readability on mobile devices.

### What prevents duplicate votes on the same comment?

The database schema defines a unique composite index on `userId` and `commentId` in the `CommentVote` model. When a user submits a vote, the **[vote/route.ts](https://github.com/f/prompts.chat/blob/main/src/app/api/prompts/%5Bid%5D/comments/%5BcommentId%5D/vote/route.ts)** handler uses an upsert operation that either creates a new vote record or deletes the existing one if the same vote value is submitted twice, effectively toggling the vote off.

### How does the shadow-ban moderation work?

When an admin flags a comment via the flag endpoint, the system sets `comment.flagged = true`. The `GET` handler in **[route.ts](https://github.com/f/prompts.chat/blob/main/src/app/api/prompts/%5Bid%5D/comments/route.ts)** filters these flagged comments from the response for regular users (lines 80-90), but explicitly includes them when the requesting user is an admin or the original author, enabling content moderation without disrupting thread context for authorized viewers.

### Can deleted comments be restored?

Yes, because the system implements soft deletion. The `DELETE` endpoint sets a `deletedAt` timestamp rather than removing the database row. While the current UI removes deleted comments and their descendants recursively from the React state via `handleCommentDeleted` in **[comment-section.tsx](https://github.com/f/prompts.chat/blob/main/src/components/comments/comment-section.tsx)** (lines 72-83), the underlying data remains in the database and could be restored by clearing the `deletedAt` field.