Systems Hub
// Real-World Comment System Engineering

Comment System মানে
শুধু table এ row insert
না — এটা ভুল।

বেশিরভাগ developer comment system বানায় তিনটা জিনিস দিয়ে: একটা table, একটা INSERT, একটা SELECT।
তারপর যখন nested reply, notification, soft delete, admin dashboard, multi-tenant filtering আসে — সব ভেঙে পড়ে।

আজকে দেখবো — কীভাবে একটা production-grade comment system বানাতে হয় যেটা লাখ user handle করতে পারে।

Nested Comments Soft Delete Push Notification Multi-tenant Filter Admin Dashboard
💬
comment system
02
// The Wrong Way

সবাই যে ভুলগুলো করে

Junior থেকে senior — এই mistakes দেখা যায় সব জায়গায়।

❌ ভুল #1 — Hard Delete
DELETE FROM comments WHERE id = ?
Data চলে গেলে recover করা যায় না। Child replies orphan হয়ে যায়।
❌ ভুল #2 — Recursive Query নেই
Nested reply fetch করতে সবাই multiple round-trip করে। N+1 query problem।
❌ ভুল #3 — Reply মানে নতুন Table
comments + comment_replies — দুটো table, দুটো service, দুটো join। Self-referential হলে একটাই যথেষ্ট।
❌ ভুল #4 — Reply এ Notification নেই
Teacher reply দিলে student জানতে পারে না। Engagement শূন্য হয়ে যায়।
❌ ভুল #5 — Admin সব comment দেখে
Multi-course platform এ admin নিজের course এর বাইরের comment দেখতে পারা — data leak।
কেন এই ভুলগুলো হয়?
// ❌ Most people do this const comment = await db.query( `SELECT * FROM comments WHERE content_id = ?`, [contentId] ); // Then for each comment, get replies: for (const c of comments) { c.replies = await db.query( `SELECT * FROM replies WHERE comment_id = ?`, [c.id] ); } // 100 comments = 101 DB queries 😱 // N+1 Problem!
এই system এ সমাধান কীভাবে?
Single classComment table, parentId self-reference, Prisma include দিয়ে একটাই query।
03
// Data Model

Self-Referential Table Design

একটাই table — parent comment এবং child reply দুটোই। parentId = null মানে root comment।

// Prisma Schema model classComment { id String @id @default(uuid()) comment String // Who wrote it (mutually exclusive) studentId String? adminId String? student Student? @relation(...) admin Admin? @relation(...) // Where it belongs classContentId String? // Admission/FRB cycleContentId String? // Academic // 🔑 Self-reference = nested reply parentId String? parent classComment? @relation("CommentReplies", fields: [parentId], ...) replies classComment[] @relation("CommentReplies") // Soft delete isDeleted Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
Visual Tree Structure
👨‍🎓
Rahim (Student) parentId: null
"এই অধ্যায়টা বুঝলাম না..."
2 hours ago
👨‍🏫
Teacher Karim (Admin) parentId: root.id
"এইভাবে ভাবো..."
1 hour ago 🔔 push sent
👨‍🎓
Rahim (Student) parentId: reply1.id
"ধন্যবাদ স্যার! বুঝেছি।"
45 min ago
Key Insight
parentId = null → root comment
parentId = someId → reply
Depth কতটাই হোক — একটাই table, একটাই model।
04
// Nested Fetch Strategy

Recursive Reply Fetch

Single content এর সব nested comment — recursive function দিয়ে unlimited depth support।

// getSingleCommentfromDb() async function getReplies(commentId) { const replies = await prisma.classComment.findMany({ where: { parentId: commentId, isDeleted: false, // soft-deleted বাদ }, orderBy: { createdAt: 'asc' }, select: selectFields, }); // Recursively fetch each reply's children return Promise.all( replies.map(async (r) => ({ ...r, replies: await getReplies(r.id), })) ); } // Root comments first (parentId: null) const parentComments = await prisma.classComment .findMany({ where: { OR: [ { classContentId: id }, { cycleContentId: id }, // dual content! ], parentId: null, isDeleted: false, }, orderBy: { createdAt: 'desc' }, select: selectFields, }); // Attach nested replies return Promise.all( parentComments.map(async (c) => ({ ...c, replies: await getReplies(c.id), })) );
কেন এই approach ভালো?
Depth unlimited — 3 level, 5 level, যতই nested হোক কাজ করবে।
Promise.all() — replies গুলো parallel এ fetch হয়, sequential না।
isDeleted filter — deleted comment automatically hide।
Dual Content Support
classContentId — Admission/FRB platform
cycleContentId — Academic platform
একটাই endpoint, দুই platform এর content handle করে।
🔒
selectFields — nestedSelectFields Helper
nestedSelectFields(["student.name", "admin.photo"]) → automatically nested Prisma select object। Over-fetching নেই।
⚠️ একটা সীমাবদ্ধতা
Deeply nested (5+ level) হলে DB round-trips বাড়ে। Fix: CTE (Common Table Expression) বা Redis cache। এই system এ comment সাধারণত 2 level — safe।
05
// Admin Dashboard

Admin Dashboard — 5টা Query একসাথে

Admin একটা request এ পায়: total, filtered, replied, pending, today — সব prisma.$transaction([]) দিয়ে।

// One DB round-trip, 5 queries parallel const [ totalCount, comments, filteredTotalCount, repliedComments, todayComments, ] = await prisma.$transaction([ // 1. সব parent comment count (unfiltered) prisma.classComment.count({ where: totalCountWhere }), // 2. Actual data (paginated, filtered) prisma.classComment.findMany({ where: listWhere, orderBy, skip, take, select: getAllselectFieldsForGetAllComments, }), // 3. Filtered total (for pagination) prisma.classComment.count({ where: listWhere }), // 4. Replied count (admin replied করেছে) prisma.classComment.count({ where: repliedCountWhere }), // 5. Today's comments (24h window) prisma.classComment.count({ where: todayCountWhere }), ]); const pendingComments = totalCount - repliedComments; // pending derive করা — আলাদা query লাগে না!
5
Queries in 1 round-trip
0
Extra pending query (derived)
📊
filterData: "pending"
replies.none { adminId: not null } — admin reply নেই এমন
filterData: "replied"
replies.some { adminId: not null } — admin reply আছে এমন
📅
today filter
createdAt: { gte: startOfToday, lt: endOfToday } — midnight to midnight
🔐
Admin Scope Filter
Admin শুধু তার assigned course এর comment দেখতে পারে
06
// Multi-Tenant Architecture

Host-Based + Role-Based Filtering

একটাই backend, তিনটা platform। কোন admin কোন comment দেখবে — hostname + adminId দিয়ে decide।

// Admin scope: শুধু assigned courses if (user?.adminId) { const accessibleCourses = await prisma.courseAdmin.findMany({ where: { adminId: user.adminId, isDeleted: false }, select: { courseId: true }, }); accessibleCourseIds = accessibleCourses .map((c) => c.courseId); } // Host-based platform separation if (hostName === config.frb_host_name) { // FRB: Academic + FRB product classContentRelation = { courseSubjectChapter: { courseSubject: { course: { AND: [ { Category: { contains: 'Academic' } }, { productName: { contains: 'FRB' } }, { id: { in: accessibleCourseIds } }, ]} }} }; } else if (hostName === config.academic_host_name) { // Academic: cycleContent path cycleContentRelation = { ... }; } else { // Admission: non-academic category classContentRelation = { ... }; }
Platform → Data Path
admission.domain.com
classContent
(Admission)
frb.domain.com
classContent
(Academic+FRB)
academic.domain.com
cycleContent
(Academic)
Security by Design
Admin A শুধু course X, Y এর comment দেখে। Admin B শুধু course Z। Cross-platform leakage impossible — DB query level এ filter।
OR Content Condition
contentConditions.length === 0 হলে empty return — unknown platform এ কোনো data expose হয় না।
07
// Soft Delete + Notification

Soft Delete & Push Notification

Delete মানে data মুছে ফেলা না — isDeleted: true করা। Reply হলে instant notification।

// ✅ Soft Delete — data থাকে, hide হয় const deleteCommentFromDb = async (id) => { await prisma.classComment.update({ where: { id }, data: { isDeleted: true }, // not DELETE! }); return {}; }; // Notification flow on reply: const replyToCommentIntoDb = async (payload) => { const result = await prisma.classComment .create({ data: replayData }); // Non-blocking notification (try/catch) try { await replayCommandSentToNotification( replayData ); } catch (error) { console.log("Notif error:", error.message); // Reply succeeds even if notif fails } return response; };
Non-blocking Design
Notification fail হলেও reply সফল হয়। try/catch দিয়ে isolate করা — critical path আলাদা।
Push Notification Flow
1. Content Info Fetch
classContentId → course → subject → chapter chain
2. Domain URL Detect
academic/admission/frb — deep link URL auto-generate
3. Sender Role Check
teacher/co-teacher/cx → আলাদা notification title বাংলায়
4. NotificationLog Create
DB তে log + bumpNotificationUserVersion() → Firebase push
🔔
শিক্ষক Karim তোমার কমেন্টের উত্তর দিয়েছেন
শিক্ষক Karim: 'এই সূত্রটা মনে রাখো...'
Deep link → course → class content → comment
08
// Reusable Helper

nestedSelectFields() — Hidden Gem

Prisma এর nested select object manually লেখা nightmare। এই helper দিয়ে string array → Prisma select object।

// nestedSelectFields.js export const nestedSelectFields = (fields = []) => { const select = {}; fields.forEach((fieldPath) => { const keys = fieldPath.split('.'); let current = select; while (keys.length > 0) { const key = keys.shift(); if (!current[key]) { current[key] = keys.length > 0 ? { select: {} } : true; } if (keys.length > 0) { current = current[key].select; } } }); return select; };
Before vs After
❌ Without helper (manual)
// 20 lines of nested objects select: { id: true, student: { select: { name: true, profilePhoto: true, } }, replies: { select: { admin: { select: { name: true } } } } }
✅ With helper (clean)
nestedSelectFields([ "id", "student.name", "student.profilePhoto", "replies.admin.name", "replies.student.name", ]) // → same nested Prisma object // Auto-generated! Error-free.
কেন এটা important?
Comment এর select field 20+ nested path। manually লিখলে typo, missing field, refactor nightmare। এই helper দিয়ে array এ path লিখো — Prisma object automatic।
09
// Validation + Restriction

Comment করার আগে 3 Layer Check

প্রতিটা comment create/reply request এ — auth → restriction → validation — তিনটা gate।

Route Definition
router.post('/', // Gate 1: JWT verify + role check authorizationMiddleware.authorize([ Enums.roles.ADMIN, Enums.roles.STUDENT, Enums.roles.SUPERADMIN, ]), // Gate 2: Zod schema validation validationRequest( CommentValidationSchema .createCommentValidationSchema ), // Gate 3: Student restriction check checkRestriction( RestrictionType.MEDIA_COMMENT ), CommentController.createComment ); // Zod: one of classContentId OR cycleContentId body.refine( (data) => data.classContentId || data.cycleContentId, { message: 'Either content id required' } );
3 Gate Flow
01
JWT Auth + Role
Token verify → student/admin/superAdmin only। Anonymous কেউ comment করতে পারে না।
authorize()
02
Zod Schema Validate
comment string required, contentId must be UUID, classContentId OR cycleContentId required।
validationRequest()
03
Student Restriction
Student কি comment করার permission আছে? Admin ban করে রেখেছে? MEDIA_COMMENT restriction check।
checkRestriction()
Zod Refinement — Smart Validation
.refine() দিয়ে cross-field validation — classContentId বা cycleContentId এর যেকোনো একটা থাকতেই হবে। Prisma পর্যন্ত invalid data পৌঁছায় না।
10
// Summary

এই Comment System কেন Best?

প্রতিটা design decision এর পিছনে কারণ আছে।

🌲
Self-Referential Model
একটাই table, unlimited depth nested reply। parent আলাদা table নেই — JOIN complexity নেই।
🗑️
Soft Delete
isDeleted: true — data হারায় না, audit trail থাকে, child reply orphan হয় না।
$transaction([5 queries])
Admin dashboard এ 5টা count/data query একটাই DB round-trip এ। performance optimal।
🔐
Multi-Tenant Security
hostname + adminId দিয়ে DB query level এ filter। cross-platform data leak impossible।
🔔
Smart Notification
Reply হলে parent commenter পায় push। Teacher/CX আলাদা বাংলা message। Deep link দিয়ে সরাসরি comment এ যায়।
🧩
nestedSelectFields Helper
20+ nested path string array → Prisma select object। Over-fetch নেই, typo নেই।
মূল কথা
Comment system ছোট মনে হয়, কিন্তু এর ভেতরে আছে — multi-tenancy, nested data, notification pipeline, security layers, admin analytics। এই patterns জানলে যেকোনো complex feature handle করতে পারবে।