Back to BlogArchitecture & Engineering

Adding Comments and @Mentions to Your SaaS

Rupak Amin

Founder & Lead Engineer, RAITHub

7 min read

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.

PartJobWhat breaks without it
Comment modelAttach a comment to any resource, scoped to the tenantA separate table per resource type, duplicated logic
Mention parsingResolve @names to real users on the serverSpoofed mentions; links that point nowhere
Permission checkConfirm the mentioned user may see the recordA mention notification leaks a private record
NotificationTell mentioned and subscribed users, per their preferencesEither 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?

OptionExamplesChoose this whenWatch out for
Embeddable comments widgetHosted commenting SaaS for websitesYou want public comments on content pages fast, with little integrationNot built for per-record, permission-scoped in-app collaboration
Collaboration platformA real-time doc or whiteboard platform with commentsComments live inside a rich editor you are already using from a vendorYou inherit their data model and permissions; hard to join to your records
Build it inThe model aboveComments attach to your own records and must respect your permissionsYou own mention safety and notification dedup; test both
Hire a team to build itRAITHub or another studioComments, mentions and notifications must agree and never leak a recordInsist 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.

CommentsMentionsCollaborationSaaSNotificationsPostgreSQL

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.