Core Resources
View as Markdown

Comment

Reviewer feedback attached to a Droplet — page-level or pinned to a specific element. Powers the in-app review loop and the AI-edit loop (`drop comments create/list/resolve/delete`).

A Comment is a piece of reviewer feedback attached to a Droplet. Comments can be page-level or pinned to a specific element via a CSS selector; they can have replies (threads); and they have a resolved flag for closing the loop.

Comments are first-class on the API as of feat/043: create / list / get / resolve / unresolve / delete, plus thumbs-up / unthumbs-up for reactions. create inherits read permission — anyone who can see the Droplet can comment on it, matching the web UI. The AI-loop primary close verb is resolve, not delete — resolve preserves the conversation history; delete is for actual removal (spam, mistakes).

Reactions today are a single type (thumbs-up). The reactionType column on CommentReaction is a Postgres enum shaped for future expansion (👏 ❤️ 😄 etc.) without a destructive migration.

The Comment object

Every Comment carries these attributes.

Attributes

id
string
CUID primary key. Stable identifier — appears in /api/comments/{id} paths and in MCP tool inputs.
appSlug
string
The slug of the parent Droplet. Returned by get but not by list (list is already scoped to a slug).
parentId
string· nullable
Non-null when this comment is a reply to another. The contract returns replies nested under their parent (comment.replies: Comment[]), so most callers never need to walk parentId themselves.
authorName
string
Display name at the time the comment was authored.
authorEmail
string· nullable
Author email at the time the comment was authored. Nullable for legacy comments.
body
string
Comment text. May include @mentions rendered as @Jane Doe in markdown export; raw @[name](email) form is the storage convention. (Tracks the UI convention from #41 mentions.)
targetSelector
string· nullable
CSS selector identifying the pinned element. null for page-level comments. The selector is captured at pin time; if the element later moves or is deleted, the stored selector may not resolve against the current HTML — that's the caller's problem to handle.
resolved
boolean
True when the comment has been marked resolved. Flip via `POST /api/comments/{id}/resolve` or `POST /api/comments/{id}/unresolve`. Resolve is the AI-loop primary close verb, not delete — preserves the audit trail.
thumbsUpCount
number
Computed at read time. Number of distinct users who have thumbs-up'd this comment. Always ≥ 0.
viewerThumbedUp
boolean
Computed at read time. True when the caller has reacted to this comment with a thumbs-up. False for unauthenticated callers and external share-link viewers.
createdAt
timestamp
ISO 8601 timestamp of creation.
updatedAt
timestamp
ISO 8601 timestamp of last metadata update (resolved-state flips bump this).
More attributes+ Show 2
targetText
string· nullable
Snippet of the pinned element's text content at pin time. Used by the UI for pin-target rendering when the selector still matches.
targetVersion
number· nullable
The Droplet version the pin was created against. Used by the UI to flag stale pins when the live version differs.
THE COMMENT OBJECT
{
  "id": "cmt_a1b2c3",
  "appSlug": "my-droplet",
  "parentId": null,
  "authorId": "usr_dan",
  "authorName": "Dan Perrera",
  "authorEmail": "dan.perrera@himarley.com",
  "body": "Can we add a third initiative around onboarding? See last week's retro.",
  "targetSelector": null,
  "targetText": null,
  "targetVersion": null,
  "resolved": false,
  "thumbsUpCount": 2,
  "viewerThumbedUp": true,
  "createdAt": "2026-06-08T22:31:54.000Z",
  "updatedAt": "2026-06-08T22:31:54.000Z"
}

Methods

Endpoints to operate on a Comment. The example switches between curl, drop CLI, and MCP via the dropdown — your choice persists across the page.

POST

Create a Comment

/api/apps/{slug}/comments

Post a comment on a Droplet. Allowed for anyone who can read it — comment creation inherits READ permission, matching the web UI. Pass parentId to reply into an existing thread, or targetSelector + targetText to pin the comment to an element.

Pinning durably takes both fields. The viewer resolves a pin by trying targetSelector first, then falling back to finding the element whose text matches targetText. A pin with a selector but no targetText has no fallback: once the selector stops resolving — which any re-deploy that shifts the DOM will do — the pin is dead. Get both from the live HTML (GET /api/apps/{slug}/versions/{v}/download, or drop download <slug>); targetText must be the element's textContent, trimmed, first 100 characters.

The Droplet version is stamped server-side. @mentions in body notify the mentioned users; any who lack access come back in inaccessibleMentions, and any auto-granted viewer access come back in grantedAccess. Returns 403 when comments are disabled on the Droplet.

Parameters

slug
string· path· required
The Droplet slug.
body
string· body· required
The comment text (1-8000 characters).
parentId
string· body
Reply into an existing thread. Omit for a top-level comment.
targetSelector
string· body
CSS selector to pin the comment to. Omit for page-level. Pair with targetText or the pin breaks on re-deploy.
targetText
string· body
The pin target's textContent, trimmed, first 100 chars. The fallback that makes a pin survive a re-deploy.
targetRect
string· body
Pin position within the target element. Set by the UI / SDK.
targetVersion
number· body
Droplet version the pin was created against.
POST/api/apps/{slug}/comments
drop comments create customer-forum-2026-product-deck --body "This slide needs a source citation for the 40% figure." --target-selector #d2-07-inquiry-assist --target-text "Inquiry Assist"
Response201
{
  "comment": {
    "id": "clx9k2p0000008l3f5g7h9k2m",
    "parentId": null,
    "authorId": "usr_jackie",
    "authorName": "Jackie Booth",
    "authorEmail": "jackie.booth@himarley.com",
    "body": "This slide needs a source citation for the 40% figure.",
    "targetSelector": "#d2-07-inquiry-assist",
    "targetText": "Inquiry Assist",
    "targetVersion": 12,
    "resolved": false,
    "thumbsUpCount": 0,
    "viewerThumbedUp": false,
    "createdAt": "2026-08-24T14:02:11.000Z",
    "updatedAt": "2026-08-24T14:02:11.000Z"
  }
}
GET

List Comments on a Droplet

/api/apps/{slug}/comments

Return all comments on a Droplet the caller can read, in chronological order (oldest first). Each comment includes thumbsUpCount and viewerThumbedUp (computed on read). Threads are returned nested under replies. Filter by resolved=true|false to scope to open or resolved comments only; omit to include both.

Parameters

slug
string· path· required
The Droplet slug.
resolved
boolean· query
Filter by resolved state. Omit to include both.
GET/api/apps/{slug}/comments
drop comments list my-droplet
Response200
[
  {
    "id": "clx9k2p0000008l3f5g7h9k2m",
    "parentId": null,
    "authorId": "usr_dan",
    "authorName": "Dan Perrera",
    "authorEmail": "dan.perrera@himarley.com",
    "body": "Can we add a third initiative around onboarding?",
    "targetSelector": null,
    "resolved": false,
    "thumbsUpCount": 2,
    "viewerThumbedUp": true,
    "createdAt": "2026-06-08T22:31:54.000Z",
    "updatedAt": "2026-06-08T22:31:54.000Z",
    "replies": []
  }
]
GET

Get a Comment

/api/comments/{id}

Fetch a single comment by id. Returns 404 if the caller cannot read the parent Droplet (no enumeration leak of comment ids).

Parameters

id
string· path· required
The comment id.
GET/api/comments/{id}
drop comments get clx9k2p0000008l3f5g7h9k2m
Response200
{
  "id": "clx9k2p0000008l3f5g7h9k2m",
  "parentId": null,
  "authorId": "usr_dan",
  "authorName": "Dan Perrera",
  "authorEmail": "dan.perrera@himarley.com",
  "body": "Can we add a third initiative around onboarding?",
  "targetSelector": null,
  "resolved": false,
  "thumbsUpCount": 2,
  "viewerThumbedUp": true,
  "createdAt": "2026-06-08T22:31:54.000Z",
  "updatedAt": "2026-06-08T22:31:54.000Z"
}
POST

Resolve a Comment

/api/comments/{id}/resolve

Mark a comment as resolved. This is the AI-loop primary close verb — preserves the conversation history (unlike delete). Allowed for anyone who can read the parent Droplet (non-destructive). Idempotent: resolving an already-resolved comment returns 204 with no body.

Parameters

id
string· path· required
The comment id.
POST/api/comments/{id}/resolve
drop comments resolve clx9k2p0000008l3f5g7h9k2m
Response200
{
  "id": "clx9k2p0000008l3f5g7h9k2m",
  "parentId": null,
  "authorId": "usr_dan",
  "authorName": "Dan Perrera",
  "authorEmail": "dan.perrera@himarley.com",
  "body": "Can we add a third initiative around onboarding?",
  "targetSelector": null,
  "resolved": true,
  "thumbsUpCount": 2,
  "viewerThumbedUp": false,
  "createdAt": "2026-06-08T22:31:54.000Z",
  "updatedAt": "2026-06-09T15:02:11.000Z"
}
POST

Unresolve a Comment

/api/comments/{id}/unresolve

Reopen a previously-resolved comment. Allowed for anyone who can read the parent Droplet. Idempotent: unresolving an already-open comment returns 204.

Parameters

id
string· path· required
The comment id.
POST/api/comments/{id}/unresolve
drop comments unresolve clx9k2p0000008l3f5g7h9k2m
Response200
{
  "id": "clx9k2p0000008l3f5g7h9k2m",
  "parentId": null,
  "authorId": "usr_dan",
  "authorName": "Dan Perrera",
  "authorEmail": "dan.perrera@himarley.com",
  "body": "Can we add a third initiative around onboarding?",
  "targetSelector": null,
  "resolved": false,
  "thumbsUpCount": 2,
  "viewerThumbedUp": false,
  "createdAt": "2026-06-08T22:31:54.000Z",
  "updatedAt": "2026-06-09T15:30:00.000Z"
}
DELETE

Delete a Comment

/api/comments/{id}

Permanently delete a comment. Cascades to its replies AND their reactions. Allowed when the caller is the comment author OR has edit permission on the parent Droplet. For actual removal only (spam, mistakes) — NOT the AI-loop close verb. Use resolve for "this has been addressed".

Parameters

id
string· path· required
The comment id.
DELETE/api/comments/{id}
drop comments delete clx9k2p0000008l3f5g7h9k2m
Response200
{
  "deleted": true,
  "id": "clx9k2p0000008l3f5g7h9k2m",
  "repliesDeleted": 2,
  "reactionsDeleted": 3
}
POST

Add a Thumbs-Up Reaction

/api/comments/{id}/thumbs-up

Add the caller's thumbs-up to a comment. Allowed for anyone who can read the parent Droplet (inherits READ permission, not comment-create). Idempotent: calling when already reacted returns 204. External share-link viewers receive 403.

Parameters

id
string· path· required
The comment id.
POST/api/comments/{id}/thumbs-up
drop comments thumbs-up clx9k2p0000008l3f5g7h9k2m
Response200
{
  "id": "clx9k2p0000008l3f5g7h9k2m",
  "thumbsUpCount": 3,
  "viewerThumbedUp": true
}
DELETE

Remove a Thumbs-Up Reaction

/api/comments/{id}/thumbs-up

Remove the caller's thumbs-up from a comment. Idempotent: calling when no reaction exists returns 204.

Parameters

id
string· path· required
The comment id.
DELETE/api/comments/{id}/thumbs-up
drop comments unthumbs-up clx9k2p0000008l3f5g7h9k2m
Response200
{
  "id": "clx9k2p0000008l3f5g7h9k2m",
  "thumbsUpCount": 2,
  "viewerThumbedUp": false
}