Skip to content
TALA
Esc
navigateopen⌘Jpreview
On this page

Comments

Project and task comment threads — what they share, what only a task comment carries, and the author-or-manager moderation rule both enforce.

Two flat comment thread types, one per work item: ProjectCommentModule at /api/projects/:projectId/comments and TaskCommentModule at /api/tasks/:taskId/comments. Both carry the same guard stack as WorkspaceAuthGuard + OrganizationGuard, optional x-organization-id header, 403 You do not belong to an active organization with no active membership.

Routes

Method Path Summary
GET /api/projects/:projectId/comments List a project’s comment thread, oldest first
POST /api/projects/:projectId/comments Post a comment on a project
PATCH /api/projects/:projectId/comments/:commentId Edit a comment
DELETE /api/projects/:projectId/comments/:commentId Soft-delete a comment
GET /api/tasks/:taskId/comments List a task’s comment thread, oldest first, with attachments
POST /api/tasks/:taskId/comments Post a comment on a task
PATCH /api/tasks/:taskId/comments/:commentId Edit a comment
DELETE /api/tasks/:taskId/comments/:commentId Soft-delete a comment

What both threads share

Body validation. body is required, 1–5000 characters, and trimmed before the length check runs:

const trim = ({ value }: { value: unknown }) =>
  typeof value === 'string' ? value.trim() : value;

export class CreateProjectCommentDto {
  @Transform(trim)
  @IsString()
  @Length(1, 5000)
  body: string;
}

Trimming in the DTO rather than in the service is deliberate: validating first and trimming afterward would accept a body of only whitespace and persist an empty string, and an empty comment row is indistinguishable from a bug.

Ordering. Both list endpoints sort createdAt ASC, id ASC — the id tiebreaker matters because two comments posted in the same millisecond must not swap places between requests.

No existence disclosure. A projectId/taskId that does not resolve to this organization, or a commentId that does not belong to that thread, is a 404, not a 403 — the same rule the rest of the API follows so a probe cannot distinguish “not yours” from “does not exist”.

Moderation. Editing or deleting is restricted to the comment’s author, or an organization OWNER/ADMIN acting on anyone’s comment:

private assertCanModerate(comment: TaskComment, userId: string, role: OrgRole, verb: 'edit' | 'delete') {
  if (comment.author?.id !== userId && role === OrgRole.MEMBER)
    throw new ForbiddenException(
      `Only the comment author or an organization manager can ${verb} this comment`,
    );
}

Both services implement this identically rather than sharing a base class — the same rule, restated once per module.

Project comments — what they deliberately don’t have

ProjectComment carries no assets relation, no replies, and no reactions:

Deliberately narrower than TaskComment: no assets M2M, because the Workspace comment composer has no asset picker and inventing one would mean shipping a control with no design. Replies and reactions are likewise absent — the page renders both, and both are disabled in place rather than faked.

{
  "message": "Comments retrieved successfully",
  "data": [
    {
      "id": "PCa7Bk9x2Q",
      "body": "Pushed the new palette to the shared library.",
      "author": { "id": "USa1B2c3D4", "fullName": "Amoako Owusu", "imageUrl": null },
      "createdAt": "2026-09-01T09:00:00.000Z",
      "updatedAt": "2026-09-01T09:00:00.000Z"
    }
  ]
}

CreateProjectCommentDto/UpdateProjectCommentDto both take body only — there is nothing else to send.

Task comments — attachments

TaskComment carries an assets many-to-many via task_comment_assets, a different grain from a task’s own asset links (task_assets — see Workspace): “this task is about these files” versus “this remark came with these files.” Neither cascades into the other.

{
  "body": "Attached the latest export for review.",
  "assetIds": ["ASa7Bk9x2Q"]
}

assetIds is optional on create (defaults to none) and, like task-asset attachment, is validated against organization-owned assets only:

{ "statusCode": 403, "message": "Comment assets must belong to the task organization" }

On PATCH, assetIds follows the same replace-or-leave contract as PUT /api/tasks/:taskId/assets: omitting the key leaves the existing attachments alone; sending [] detaches all of them. Without this, a typo’d attachment at create time could only be fixed by deleting the whole comment — UpdateTaskCommentDto exists specifically to carry assetIds too.

{
  "message": "Comments retrieved successfully",
  "data": [
    {
      "id": "TCa7Bk9x2Q",
      "body": "Attached the latest export for review.",
      "author": { "id": "USa1B2c3D4", "fullName": "Amoako Owusu", "imageUrl": null },
      "assets": [
        {
          "id": "ASa7Bk9x2Q",
          "assetName": "wordmark-final.png",
          "assetType": "image/png",
          "currentVersion": 2,
          "sizeBytes": 48213,
          "isFavorite": false,
          "createdBy": { "id": "USa1B2c3D4", "fullName": "Amoako Owusu", "email": "amoako@example.com" },
          "collectionId": "CLa7Bk9x2Q",
          "projectId": "PRa7Bk9x2Q",
          "createdAt": "2026-08-30T12:00:00.000Z",
          "updatedAt": "2026-09-01T08:55:00.000Z"
        }
      ],
      "createdAt": "2026-09-01T09:00:00.000Z",
      "updatedAt": "2026-09-01T09:00:00.000Z"
    }
  ]
}

Errors

Situation Status Message
Project/task in another organization, or unknown id 404 Project not found / Task not found
Comment does not belong to the given thread, or unknown id 404 Comment not found
Edit/delete by someone who is neither the author nor OWNER/ADMIN 403 Only the comment author or an organization manager can edit this comment / ...delete this comment
A task-comment attachment does not belong to the task’s organization 403 Comment assets must belong to the task organization
No active membership in the resolved organization 403 You do not belong to an active organization

Was this page helpful?