Founder & Lead Engineer, RAITHub
RAITHub ships and tests production software. See QA as a Service or talk to us.
Comments let users discuss a record in place; @mentions pull a specific person in. Build a comment model that attaches to any resource, resolve mentions on the server from stable user IDs rather than the typed text, check the mentioned person may see the record before notifying them, and notify only the right people. The traps are spoofed mentions and leaking a record to someone who should not see it.
If you would rather have comments and mentions built for you, see how RAITHub would build this below.
What does a comments feature actually need?
More than a text column. It needs to attach to different resource types, thread or at least order cleanly, support edit and delete with history, render mentions safely, and notify the right people. Teams ship the textbox, then discover the real work is mentions, permissions and notifications.
| Part | Job | What breaks without it |
|---|---|---|
| Comment model | Attach a comment to any resource, scoped to the tenant | A separate table per resource type, duplicated logic |
| Mention parsing | Resolve @names to real users on the server | Spoofed mentions; links that point nowhere |
| Permission check | Confirm the mentioned user may see the record | A mention notification leaks a private record |
| Notification | Tell mentioned and subscribed users, per their preferences | Either silence or spam |
How should comments be stored to attach to anything?
Use a polymorphic reference: the comment names the type and id of what it is attached to, scoped to the workspace. One table serves tasks, invoices, documents and anything you add later.
CREATE TABLE comments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL,
subject_type text NOT NULL, -- 'task', 'invoice', 'document'
subject_id uuid NOT NULL,
author_id uuid NOT NULL,
body text NOT NULL, -- stored as written; rendered safely
parent_id uuid, -- for one level of threading
edited_at timestamptz,
deleted_at timestamptz, -- soft delete, keeps thread intact
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON comments (workspace_id, subject_type, subject_id, created_at);
-- Resolved mentions, so notifications and rendering do not re-parse text.
CREATE TABLE comment_mentions (
comment_id uuid NOT NULL,
user_id uuid NOT NULL,
PRIMARY KEY (comment_id, user_id)
);
Soft-delete comments so a deleted message does not orphan its replies; show "comment deleted" in the thread. Store the raw body and render it safely at display time, escaping HTML so a comment can never inject markup.
How do you parse @mentions without letting them be spoofed?
Resolve mentions on the server to stable user IDs, and store those IDs. Never trust the display text, and never let a user mention someone by typing a name that resolves to a different account. The client sends the chosen user's ID alongside the text; the server verifies it.
// The editor sends { body, mentionedIds }. The server trusts the IDs, not the text,
// and keeps only the users who are real members of this workspace.
export async function createComment(db: Tx, input: {
workspaceId: string; authorId: string; subjectType: string; subjectId: string;
body: string; mentionedIds: string[]
}) {
const members = await membersOf(db, input.workspaceId, input.mentionedIds) // filter to real members
const comment = await insertComment(db, input)
for (const userId of members) {
if (await canSee(db, userId, input.subjectType, input.subjectId)) { // must be allowed to see it
await insertMention(db, comment.id, userId)
}
}
return comment
}
The double check matters: the mentioned user must be a member of the workspace, and must be allowed to see the specific record. Mentioning someone who cannot see the task, and thereby emailing them its contents, is a real data leak. Use the same permission check as the rest of the app, from building a roles and permissions UI.
Who gets notified, and how do you avoid noise?
Notify mentioned users and thread subscribers, each according to their preferences, and deduplicate so one comment never sends two emails.
- Mentioned users get a direct notification: "Sam mentioned you on invoice 1042".
- Subscribers (the record's assignee, people who already commented) get a quieter "new comment" notification they can turn off.
- Deduplicate. If someone is both mentioned and a subscriber, send one notification, not two.
- Respect the author. Do not notify people of their own comment.
Route all of this through the notification layer so preferences and channels are honoured in one place, as in building a SaaS notification system. A mention becomes an event; the notification system decides in-app, email or both.
Buy, build or hire?
| Option | Examples | Choose this when | Watch out for |
|---|---|---|---|
| Embeddable comments widget | Hosted commenting SaaS for websites | You want public comments on content pages fast, with little integration | Not built for per-record, permission-scoped in-app collaboration |
| Collaboration platform | A real-time doc or whiteboard platform with comments | Comments live inside a rich editor you are already using from a vendor | You inherit their data model and permissions; hard to join to your records |
| Build it in | The model above | Comments attach to your own records and must respect your permissions | You own mention safety and notification dedup; test both |
| Hire a team to build it | RAITHub or another studio | Comments, mentions and notifications must agree and never leak a record | Insist the handover proves a mention cannot reach someone who cannot see the record |
Do-it-yourself estimate: 4–7 days for the polymorphic comment model, safe rendering, server-resolved mentions with permission checks and deduplicated notifications, if auth and a notification layer exist. The main risk is a mention that notifies, and so exposes a record to, someone without access to it.
How RAITHub would build this
As part of a new SaaS build, or added to an existing product, scoped in writing after the free audit.
- Polymorphic comments: one model attaching to any resource, with threading, edit history and soft delete.
- Safe mentions: resolved on the server to user IDs, verified as members, and checked against the record's permissions.
- Safe rendering: raw body stored, escaped and rendered so a comment cannot inject markup.
- Deduplicated notifications to mentioned users and subscribers, through the notification layer and their preferences.
Timeline: inside a new product, this is part of the 4–6 week fixed-scope SaaS build; added to an existing backend, it fits the 6–12 week backend range, with the exact scope in the quote.
You receive: automated tests and CI, including a test that a mention cannot leak a record, handover docs, and full IP assigned to you under NDA.
Next step: a free 15-minute technical audit, then a written fixed quote. See the SaaS development service, or book the audit.
Frequently asked questions
How do I store comments that can attach to different record types?
Use one polymorphic table where each comment names the subject type and id it is attached to, scoped to the workspace. One model then serves tasks, invoices, documents and anything you add later, instead of a separate comments table per feature.
How do I stop @mentions being spoofed?
Resolve mentions to stable user IDs on the server, from what the editor selected, and store those IDs rather than the typed text. Verify each is a real workspace member, so nobody can mention a name that resolves to a different account.
Can an @mention leak private data?
Yes, if you notify a mentioned user without checking access. Before creating the mention and its notification, confirm the user is allowed to see the specific record. Otherwise a mention emails the record's contents to someone who should not have them.
Who should get notified about a comment?
Mentioned users directly, and thread subscribers more quietly, each according to their notification preferences. Deduplicate so someone both mentioned and subscribed gets one notification, and never notify people about their own comment.
Should comments be deleted or soft-deleted?
Soft-delete them, so a removed comment does not orphan its replies. Mark it deleted and show a placeholder in the thread, which keeps the conversation readable while honouring the user's request to remove their message.
Related posts
Ready to discuss your project?
Book a free 15-minute technical audit with our engineering team.