Threads and updates
A resource may contain many threads. Replies belong to the thread. There is no nested reply tree and no parentCommentId. version starts at 1. activitySeq starts at 0 and becomes 1 on the root comment.
Threads list newest activity first: lastActivityAt DESC, then id DESC. lastActivityAt moves when a comment is created. Edits, resolve, reopen, and deletion do not move it. Comments list oldest first: createdAt ASC, then id ASC.
Cursors are opaque base64url JSON { "v": 1, "t": "<ISO-8601>", "id": "<row id>" }. Default page size is 50. limit is an integer from 1 through 100. Above 100 is validation_error. A short page returns nextCursor: null. The client dedupes by id. Load more stays available while nextCursor is non-null.
Filters all, open, resolved, and unread run before the limit. Body text is plain. A whitespace-delimited token renders as a link only when it parses as http or https. Other schemes stay text. Anchors use rel="noopener noreferrer". The stored body stays plain text. There is no HTML and no markdown.
Hosted subscribe opens GET /api/v1/stream for that tuple, with optional threadId. The publishable key and viewerToken travel on the query because EventSource cannot set an authorization header. The secret key is never in that query. Payload is ids only: threadId, commentId, eventId. Heartbeat is every 20 seconds. The stream closes at token expiry and on scope change.
If subscribe is omitted or the stream errors, the widget polls every 4 seconds, pauses while the document is hidden, and refreshes on focus and after a local write. Reconnect backoff is 4s, 8s, 16s, then 60s. Concurrent SSE listeners are capped at 20 per workspace per process. Several replicas do not share that cap.
Pending submits stay disabled until the request settles. On validation, conflict, rate limit, quota, forbidden, or a network failure, the composer text and mention chips stay. Optimistic rows roll back or show a failed state with retry.
Related: Limits · Unread · API overview