When your website grows, comments become a bottleneck. Built-in solutions like Disqus are a trade-off: you lose data control, page load speed suffers, and third-party ads annoy users. Server throughput decreases, infrastructure costs rise. We build custom comment systems on Laravel and React that solve N+1 queries, spam, and complex moderation. In 3–7 days you get a flexible, fast, scalable system fully under your control.
What Problems Does a Custom Comment System Solve?
N+1 Queries for Nested Comments
A typical mistake: loading replies with separate queries for each comment. On a page with 100 comments, that's 101 queries. The solution: eager loading with recursive fetching — one query retrieves the entire tree, and Laravel builds the hierarchy in memory. As noted in the Laravel documentation, proper relationship handling reduces queries to a minimum.
Spam and Moderation
We automatically filter comments with suspicious links, set rate limits (10 comments per minute), and send suspicious ones for manual review. For guests, we use CAPTCHA. Details can be seen below.
More About Moderation
We combine pre-moderation for new users and automatic rule-based checks. Comments containing more than two external links are automatically marked as spam. For registered users with high reputation, comments are published immediately.Duplicate Likes
To prevent multiple votes from the same user, we use Redis caching with key comment_like:{comment_id}:{user_id}. This is more reliable than a separate table and faster.
How We Do It
Tech Stack
- Backend: Laravel, PHP 8.3, PostgreSQL 16, Redis 7
- Frontend: React 18, TypeScript, Tailwind CSS
- Infrastructure: Docker, Nginx, GitHub Actions
Key Solution: Anti-N+1 Queries
// In the controller
$comments = Comment::where('entity_type', $entityType)
->where('entity_id', $entityId)
->whereNull('parent_id')
->with(['user', 'replies' => function ($query) {
$query->where('status', 'approved')->with('user');
}])
->latest()
->paginate(20);
This pattern reduces the number of queries from N+1 to 3 (root comments, replies, users).
Database Schema
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
entity_type VARCHAR(50) NOT NULL, -- 'article', 'product', 'post'
entity_id INTEGER NOT NULL,
parent_id INTEGER REFERENCES comments(id) ON DELETE SET NULL,
user_id INTEGER REFERENCES users(id),
author_name VARCHAR(100), -- for guests
author_email VARCHAR(255),
body TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending|approved|rejected|spam
likes_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ON comments(entity_type, entity_id, status, created_at);
CREATE INDEX ON comments(parent_id);
CREATE INDEX ON comments(user_id);
API and Frontend Design
Laravel API
class CommentController extends Controller
{
// Fetch comments for an entity
public function index(Request $request, string $entityType, int $entityId): JsonResponse
{
$comments = Comment::where('entity_type', $entityType)
->where('entity_id', $entityId)
->where('status', 'approved')
->whereNull('parent_id')
->with(['user:id,name,avatar', 'replies' => fn($q) => $q->where('status', 'approved')->with('user:id,name,avatar')])
->latest()
->paginate(20);
return response()->json($comments);
}
// Add a comment
public function store(StoreCommentRequest $request, string $entityType, int $entityId): JsonResponse
{
$this->throttle('comments', 10, 60);
$requiresModeration = !auth()->check()
|| auth()->user()->comments()->where('status', 'spam')->exists()
|| $this->containsSuspiciousLinks($request->body);
$comment = Comment::create([
'entity_type' => $entityType,
'entity_id' => $entityId,
'parent_id' => $request->parent_id,
'user_id' => auth()->id(),
'author_name' => auth()->user()?->name ?? $request->author_name,
'author_email' => auth()->user()?->email ?? $request->author_email,
'body' => $this->sanitize($request->body),
'status' => $requiresModeration ? 'pending' : 'approved',
]);
if ($comment->status === 'approved') {
$this->notifyParentAuthor($comment);
} else {
Notification::send(
User::moderators()->get(),
new CommentPendingNotification($comment)
);
}
return response()->json(CommentResource::make($comment), 201);
}
private function sanitize(string $body): string
{
return strip_tags($body, '<b><i><em><strong><a><br><p>');
}
private function containsSuspiciousLinks(string $body): bool
{
preg_match_all('/<a[^>]+href=["\']?([^"\'> ]+)/i', $body, $matches);
foreach ($matches[1] ?? [] as $url) {
if (!str_contains($url, config('app.url'))) {
return true;
}
}
return false;
}
}
React Comment Tree
interface Comment {
id: number;
user: { name: string; avatar: string } | null;
author_name: string;
body: string;
likes_count: number;
created_at: string;
replies?: Comment[];
}
function CommentThread({ entityType, entityId }: { entityType: string; entityId: number }) {
const { data, isLoading } = useQuery({
queryKey: ['comments', entityType, entityId],
queryFn: () => api.get(`/comments/${entityType}/${entityId}`),
});
return (
<section aria-label="Comments">
<h2>Comments ({data?.meta.total ?? 0})</h2>
<CommentForm entityType={entityType} entityId={entityId} />
{isLoading ? <CommentSkeleton /> : (
<ul className="comment-list">
{data?.data.map(comment => (
<CommentItem key={comment.id} comment={comment} depth={0} />
))}
</ul>
)}
</section>
);
}
function CommentItem({ comment, depth }: { comment: Comment; depth: number }) {
const [showReplyForm, setShowReplyForm] = useState(false);
return (
<li className={`comment depth-${depth}`}>
<img
src={comment.user?.avatar || '/default-avatar.png'}
alt={comment.user?.name || comment.author_name}
width={40} height={40}
/>
<div className="comment__content">
<header>
<strong>{comment.user?.name || comment.author_name}</strong>
<time dateTime={comment.created_at}>
{new Date(comment.created_at).toLocaleDateString('en-US')}
</time>
</header>
<p>{comment.body}</p>
<footer>
<LikeButton commentId={comment.id} count={comment.likes_count} />
{depth < 3 && (
<button onClick={() => setShowReplyForm(!showReplyForm)}>Reply</button>
)}
</footer>
{showReplyForm && (
<CommentForm parentId={comment.id} onSubmit={() => setShowReplyForm(false)} />
)}
{comment.replies?.map(reply => (
<ul key={reply.id}><CommentItem comment={reply} depth={depth + 1} /></ul>
))}
</div>
</li>
);
}
Why Custom Over Off-the-Shelf Widgets?
| Criterion | Custom Solution | Disqus / Commento |
|---|---|---|
| Data control | Full | None (third-party) |
| Performance | Optimized for your stack | Extra HTTP request |
| Customization | Unlimited | Limited |
| Cost | Development, then free | Freemium / ads |
| GDPR / Privacy | Full compliance | Risks |
| Implementation variant | Nesting | Moderation | Likes | Timeline |
|---|---|---|---|---|
| Basic | Flat | Automatic | No | 3-4 days |
| Standard | Up to 3 levels | Automatic + manual | Yes | 5-7 days |
| Premium | Up to 5 levels | Custom rules + AI filter | Yes + notifications | Up to 10 days |
Process
- Analysis — discuss requirements: nesting, moderation, notifications, auth integration.
- Design — DB schema, API endpoints, React components.
- Implementation — backend (Laravel), frontend (React), tests.
- Testing — load testing (1000 comments), spam filter verification.
- Deployment — Docker image, CI/CD, cache configuration.
Timeline and Cost
- Basic system (flat, moderation): 3–4 days
- With nesting, likes, notifications: 5–7 days
- With social integration and custom anti-spam: up to 10 days
Cost is calculated individually — we assess your project within an hour. A custom comment system pays off in 2–4 months compared to paid subscriptions. Get a consultation on integrating a comment system into your project. For an accurate estimate, contact our engineer.
A custom comment system is an investment in user experience quality and independence from third-party services. We design for your scale: from a small blog with 50 comments per day to high-load platforms with 10,000 messages per day. The architecture is the same; only Redis caching parameters and horizontal scaling strategy change.
What's Included
- Backend source code (Laravel) and frontend (React) with unit tests
- API documentation and database structure
- Docker environment setup and CI/CD pipeline
- Integration with your authentication system
- Access to a private repository
- Team training (2–3 hours)
- One month of technical support after launch
7+ years of web development experience, 50+ projects with custom comment systems. We guarantee transparency and quality.







