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

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 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 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 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 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 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 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 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 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 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 (lines 72-83), the underlying data remains in the database and could be restored by clearing the deletedAt field.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →