Comments, Threading & @Mentions
If you're building a collaborative B2B SaaS in 2026, you'll need comments — on documents (Notion / Google Docs), records (CRM / project tasks), assets (Figma / Loom), code (GitHub PRs), or content (blog moderation). The naive approach: a <textarea>, store strings, render them. The structured approach: rich text, threading, @mentions with notifications, edit/delete with history, soft-delete, real-time updates, moderation, and accessibility. Comments look simple from outside; they're easily a 3-week feature done right.
1. Decide the comment model — flat, threaded, or nested
Pick the right comment model.
Flat (linear list):
- Single timeline, no replies
- Examples: Slack channel, Twitter timeline
- Pro: simple, fast to ship
- Con: no conversation structure
Threaded (one-level reply):
- Comment with replies underneath
- Examples: Linear comments, Slack threads, GitHub PR comments
- Pro: conversation grouping; bounded depth
- Con: replies-of-replies break the model
Nested (deep tree):
- Comments with arbitrary nesting depth
- Examples: Reddit, Hacker News
- Pro: matches conversation flow
- Con: deeply nested becomes unreadable; harder to render
Inline / contextual:
- Comments anchored to specific UI elements (paragraph, line of code, image region)
- Examples: Google Docs (anchored), Figma (pinned), GitHub line comments
- Pro: context preserved
- Con: complex UX (resolving, archiving, moving with content)
For [USE CASE], pick:
- Document collaboration → inline + threaded (Google Docs style)
- Task / record discussion → threaded (Linear style)
- Public commenting → nested (Reddit) or threaded (most B2B)
- Activity log / log only → flat
Output:
1. Recommended model
2. Max depth (if nested) — typically 1-3 levels
3. Anchoring strategy (if inline)
4. Schema sketch
5. Example UI from a known product
The pragmatic default for B2B SaaS: threaded one-level. Comment + replies underneath. Limits depth, keeps UI scannable. Add inline/anchored later if needed.
2. Schema design — built to evolve
Design the comments database schema.
Core fields:
- id (uuid)
- author_user_id (fk to users)
- parent_resource_type (e.g., 'task', 'document', 'lead')
- parent_resource_id (the thing being commented on)
- parent_comment_id (nullable; null for top-level, set for replies)
- body_text (raw text)
- body_rich (rich-text JSON if applicable, e.g., Lexical / TipTap)
- created_at, updated_at, deleted_at (soft delete)
- edited (boolean; true if updated after creation)
- is_resolved (for threaded comments that can be resolved)
- thread_id (denormalized; same for parent + all replies for fast query)
Indexes:
- (parent_resource_type, parent_resource_id, created_at)
- (thread_id, created_at)
- (author_user_id, created_at)
Relations table for @mentions:
- comment_mentions
- comment_id, mentioned_user_id, mentioned_at
Reactions table (optional):
- comment_reactions
- comment_id, user_id, emoji
Anchoring (for inline comments):
- anchor_data (JSON: range / coordinates / element ID)
Multi-tenancy:
- tenant_id / org_id on every row (for SaaS)
- RLS policies (Postgres) or query-level filtering
Audit / moderation:
- flagged_count, hidden_at, hidden_by_user_id
- For UGC: report_count, last_reported_at
Output:
1. Postgres schema (DDL)
2. Indexes for common queries
3. RLS policies for multi-tenant
4. Soft-delete vs hard-delete decision
5. Migration path to threading if you ship flat first
The "thread_id" denormalization: makes "show me all comments on task X" + "expand thread" both fast. Without it, recursive CTEs get expensive.
3. Rich text vs plain text
Decide rich-text or plain-text for comments.
Plain text:
- Simple textarea
- Pro: easy; sanitize-once; no XSS surface
- Con: no formatting; users want bold/italic/links/code blocks
Markdown:
- User types **bold**, server renders to HTML
- Pro: lightweight; sanitize-after-render
- Con: not WYSIWYG; users have to know markdown
Rich text editor (TipTap / Lexical / Slate):
- WYSIWYG bold/italic/lists/links/mentions/embeds
- Pro: best UX; familiar to Google Docs / Notion users
- Con: complex; editor bugs; need sanitization
- Recommended for serious B2B SaaS
Hybrid:
- Markdown for input + WYSIWYG for display
- Used by: GitHub, Slack
- Best of both worlds with care
For 2026 default: TipTap or Lexical for new B2B SaaS.
Storage:
- Store as JSON (TipTap doc) + render to HTML on display
- Or: store HTML directly with sanitization (DOMPurify / sanitize-html)
- Always sanitize before render
Output:
1. Recommendation
2. Editor library choice
3. Storage format (JSON / HTML / Markdown)
4. Sanitization library + config
5. Allowed elements / attrs (for HTML mode)
The XSS rule: never render user-submitted HTML without sanitization. DOMPurify (client) + sanitize-html (server) are standard. See rich-text-editor-implementation-chat.md for editor details.
4. @Mentions — autocomplete + notifications
Implement @mentions.
Trigger:
- User types @
- Show dropdown with up to 10 people
- Filter as user types
- Up/down arrows + Enter to select; Escape to dismiss
- Tab to autocomplete first match
Search backend:
- /api/mentions/search?q=jo&context_id=task-123
- Return filtered users (those with access to context)
- Don't expose users from other orgs (multi-tenant)
- Include avatar + name + role
Mention storage:
- Mention rendered as a special node in rich-text JSON
- Or: rendered as @[user-id] in markdown
- Or: rendered as <mention data-id="..." /> in HTML
Mention notification:
- Server-side detect mentions on comment save
- Insert into notifications table
- Email + in-app notification per mention preferences
- Don't notify if author == mentioned user (don't ping yourself)
Edge cases:
- Mention removed by editor (after a save) → revoke notification (debatable)
- Mentioned user lost access to resource → don't link / hide
- @everyone / @channel — only for trusted authors; rate-limit
Search libraries:
- TipTap mention extension (built-in)
- Lexical mention plugin
- Roll-your-own with combobox
Output:
1. Dropdown UI
2. Search API with multi-tenant filter
3. Mention storage (in rich-text doc)
4. Notification dispatch
5. Permission check on read (don't link to inaccessible resources)
The multi-tenant @mentions trap: a search query "Jo" returns Joe across all orgs if you don't filter by tenant. That leaks user identity across customers.
5. Real-time updates
For collaborative tools, comments should appear without refresh.
Real-time comment updates.
Stack options:
- Supabase Realtime (Postgres LISTEN/NOTIFY auto-piped)
- Convex (live queries built-in)
- Pusher / Ably / Liveblocks (managed)
- WebSocket / SSE custom (Node + Redis)
Pattern:
1. User A posts comment → INSERT to DB → broadcast event
2. User B subscribed to channel → receives event → updates UI
Channel naming:
- comments:resource-type:resource-id
- e.g., comments:task:abc123
Subscriber filtering:
- Server-side: only broadcast to users with access to resource
- Or: client subscribes to specific channel + server checks on each event
Optimistic UI:
- User submits comment → appears immediately
- Server confirms via realtime event (de-dup by client-temp-id)
- On error: rollback + toast
Performance:
- Don't broadcast every keystroke (only on save / submit)
- Debounce typing-indicators if implemented (3 sec interval)
- Channel cleanup on unmount
Typing indicators (optional):
- "Joe is typing..."
- Debounced 3-sec window
- Multi-user merge ("Joe and Lisa are typing")
Output:
1. Realtime stack choice
2. Channel naming + subscriber permissions
3. Optimistic update pattern
4. Typing indicator (if implementing)
5. Error / reconnect handling
The typing-indicator gotcha: shows in 80% of comment products as polish. But it adds infrastructure (extra channel, debouncing, multi-user merge). Skip until v2 unless real-time collab is core.
6. Edit + delete + history
Implement edit + delete for comments.
Edit:
- Author can edit their own comment
- Other users can't edit
- Show "edited" indicator after edit
- Optional: edit history (audit trail)
Edit history (advanced):
- comment_edits table: comment_id, body, edited_at, edited_by
- "Show edit history" link reveals modal
- Most products skip this; some require for compliance
Delete:
- Soft delete (recommended): set deleted_at, body becomes "[deleted]"
- Replies to deleted comment still visible (otherwise tree breaks)
- Author can delete; admins / moderators can delete others
- Hard delete only after retention period (or never; audit logs)
Permissions:
- Author: edit + delete own
- Admin / org owner: edit + delete any
- Other users: read only
- RBAC at API level + UI hints
Time-based limits:
- Edit window: forever or 5 min (Slack uses 'no edit after few mins for spam'; Linear allows always)
- Delete window: same considerations
UI:
- Three-dot menu on each comment with Edit / Delete / Report / Copy link
- Hover or always visible (depends on density)
Output:
1. Permissions matrix
2. Soft-delete schema
3. Edit indicator UI
4. History feature (if required for compliance)
5. Time-window config
The "edited 5 min ago" subtlety: if you show "edited" forever, every old comment looks fresh-edited. Fade to "edited 5 min ago" on first hover. Don't surface forever.
7. Notification preferences
Comments generate noise. Let users tune.
Implement comment notification preferences.
Notification triggers:
- @mentioned in a comment
- Replied to a comment you authored
- New comment on a thread you're following
- New comment on a resource you're watching
User-level preferences:
- @mentions: always notify (most products force this)
- Replies: configurable
- Following / watching: opt-in
- Email frequency: instant / daily digest / off
- In-app vs email vs both
Per-resource preferences:
- "Watch this task" / "Mute this thread"
- Override default
Preference storage:
- user_notification_preferences (user_id, type, channel, enabled)
- thread_subscriptions (user_id, resource_type, resource_id, status)
UI:
- Settings page with toggles
- Per-thread "Mute" / "Watch" button
Email design:
- Subject: "Joe mentioned you in [task name]"
- Body: comment excerpt + link + reply-by-email (advanced)
- Reply-by-email parsing: complex but powerful
Push / mobile:
- Same prefs apply
- Service worker for browser push or native mobile
Output:
1. Preference schema
2. Default settings (sensible)
3. Email + push templates
4. Reply-by-email parsing (if enabled)
5. Unsubscribe link compliance
The default-settings rule: opt-out for @mentions (always notify), opt-in for thread subscriptions. Otherwise people get drowned.
8. Resolve threads
For comments that initiate work / discussion, resolution closes the loop.
Implement comment resolution.
Concept: a comment thread can be "open" or "resolved."
- Resolved threads can be hidden (filter)
- Resolved threads can be re-opened
- Often used for code review (GitHub), document feedback (Google Docs), task discussion
Schema:
- is_resolved boolean (or status enum)
- resolved_at, resolved_by_user_id
UI:
- "Resolve" button on threaded comment
- Visual: greyed-out style for resolved
- Filter toggle: "Show resolved" / "Hide resolved"
Permissions:
- Anyone with comment access can resolve
- Or: only author / mentioned can resolve
- Configurable per product
Re-opening:
- "Reopen" button on resolved comment
- Logged in audit
Notifications:
- Notify thread author + participants when resolved
- Skip for trivial threads
Common products:
- GitHub: resolve conversation in PRs
- Google Docs: resolve document comments
- Linear: resolve thread on issue
- Notion: resolve comment
Output:
1. Resolution schema
2. UI (resolve button + indicator)
3. Filter UX (default show open)
4. Permission rules
5. Notification trigger
The resolved-default rule: hide resolved by default. Show on demand. Otherwise resolved threads pollute the view.
9. Moderation — UGC concerns
For B2B internal use, moderation is light. For public / customer-facing comments, plan moderation.
Plan comment moderation (for customer-facing UGC).
Levels:
Level 1: B2B internal (most B2B SaaS)
- Trust internal users
- Retroactive moderation (admin can delete)
- Audit log of moderation actions
- Skip pre-moderation
Level 2: Customer + internal mixed
- Customer comments visible to other customers
- Internal users can moderate
- Report button → flagged comments queue
- Admins review + take action
Level 3: Public UGC (community / forum)
- Heavy moderation needed
- Pre-moderation queue (new users) or post-moderation
- Auto-detection: profanity filter, link spam, NSFW
- Trust scoring: long-tenured users moderate later
Tools:
- Profanity filter (e.g., bad-words npm; localized)
- Spam detection (Akismet, custom ML)
- NSFW image detection (cloud APIs)
- LLM-based moderation (OpenAI Moderation API; Anthropic)
Workflow:
- Flagged → moderator queue
- Moderator: keep / delete / ban user
- User notification on action
- Appeal process (for serious cases)
Legal:
- COPPA (under-13 users)
- DSA (EU Digital Services Act)
- Section 230 (US — platform protections)
- Region-specific UGC laws
Output:
1. Moderation level for [USE CASE]
2. Pre / post / no moderation
3. Tooling stack (profanity / spam / NSFW)
4. Moderator queue UI
5. Audit trail + legal compliance
The 2026 reality: LLM-based moderation (OpenAI Moderation API, Anthropic) outperforms regex / keyword filters for nuance. Layer them.
10. Accessibility — keyboard, screen reader, focus
Make comment UIs accessible.
Required:
- Comments in semantic HTML (article + author info)
- Time elements use <time datetime="..."> for screen readers
- Reply form: clearly labeled <textarea> + submit button
- Reply button has aria-label including comment ID
- @mention dropdown: combobox pattern with aria-expanded, aria-activedescendant
- Notifications: aria-live polite for new comments arriving
Keyboard:
- Tab through comments + reply buttons
- Enter to submit reply (if no shift, otherwise newline)
- Cmd/Ctrl+Enter to submit (alternative)
- Arrow keys in mention dropdown
- Escape to close mention dropdown / cancel reply
Focus management:
- After posting comment: focus moves to new comment + announces
- After deleting: focus moves to next comment or back to list
- Reply form opening: focus moves to textarea
Screen reader announcements:
- New comment via realtime: "[Author] commented at [time]: [excerpt]"
- Mention received: "[Author] mentioned you in [resource]"
Common failures:
- @mention dropdown using divs with click handlers (not keyboard)
- Reply form opens but focus doesn't move to it
- Real-time new comments appear silently (screen reader users miss)
- Time stamps as "5m ago" without underlying datetime attribute
Output:
1. Comment component HTML structure
2. Mention dropdown ARIA pattern (combobox)
3. Focus-management on actions
4. Live-region announcements
5. Test with VoiceOver + NVDA + keyboard-only
The mention dropdown is hard. WAI-ARIA combobox pattern is the spec. TipTap / Lexical mention extensions handle this; rolling your own requires careful implementation.
What Done Looks Like
A v1 commenting system for B2B SaaS in 2026:
- Threaded one-level comments (parent + replies)
- Rich text editor with mention support (TipTap / Lexical)
- @mentions with autocomplete + notifications
- Real-time updates via Supabase / Convex / Pusher
- Edit / delete with author + admin permissions
- Soft delete (deleted_at)
- Notification preferences (email + in-app)
- Thread resolution (open / resolved with filter)
- XSS sanitization (DOMPurify / sanitize-html)
- Multi-tenant scoping on @mention search
- Accessibility: keyboard + screen reader support
Add later when product is mature:
- Inline / anchored comments (Google-Docs-style)
- Edit history with audit trail
- Reactions / emoji
- Reply-by-email
- Typing indicators
- LLM-based moderation
- Comment search across resources
The mistake to avoid: rolling your own rich-text editor. TipTap and Lexical solved this. Use them.
The second mistake: deep nesting. >3 levels of nesting is unreadable. Stop at 1-2.
The third mistake: forgetting multi-tenant filter on @mention search. Cross-tenant user identity leak; potential GDPR violation.
See Also
- Rich-Text Editor Implementation — TipTap / Lexical setup
- Real-Time Collaboration — collaboration plumbing
- Notification Preferences & Unsubscribe — pref management
- In-App Notifications — notification surface
- Mobile Push Notifications — mobile alerts
- Email Template Implementation — email comment notifications
- WebSocket / SSE Implementation — realtime transport
- Roles & Permissions — comment permissions
- Audit Logs — comment moderation audit
- Content Moderation Pipeline — UGC moderation
- Captcha & Bot Protection — abuse defense
- Multi-Tenancy — scoping comments to tenants
- VibeReference: OpenAI Moderation — moderation API
- VibeReference: Components — UI primitives
- LaunchWeek: Customer Advisory Board — internal feedback context