Cloud|

Chat system conversations

Access, dependencies, and API behavior for Chat conversations attached to resource providers.

17 min read Updated 2026-10-30 #chat#permissions#operations

Chat attaches a conversation to a resource container and a thread to an item. Applications opt in through their catalog's discussions role. Built-in and third-party providers use the same contract. Chat lists these conversations under Discussions. Item directories and message links work in the full page and below /app/chat/panel. Providers can also offer an activity feed. Existing comments stay in their owning application.

The item discussion capabilities expose only provider-backed item threads. Keep the item and container types' declared readers available under the incoming authority alongside the Chat operation; missing current owner context fails closed. Chat stores the calling surface on capability posts and edits; direct edits retain an existing mark. The additive nullable message column does not backfill old messages. Comments and the unreleased Compose profile remain until their separately coordinated release.

Open a conversation

All paths below start with /api/chat.

Request Result
GET /system-chats A fair page from the catalog's discussion providers, including containers with no conversation yet.
POST /system-chats/:resourceType/:resourceId/open Create or return the container's conversation. The type must be a container declared by a discussion provider.
POST /conversations/:conversation/threads/for-resource with { "ref": { "type": "inventory.item", "id": "…" } } Create or return an item thread. The type must be the provider's discussion item type. IDs are opaque strings up to 512 characters.
GET /conversations/:conversation/threads Authorized threads, with optional filter=open|done|unread, q, and cursor.

Message uploads and downloads use the same routes and limits as ordinary chats: see Chat message attachments. An upload's optional thread is checked with the item reader. A sent attachment's download and shared-list entry require the current container and item visibility of its message.

System conversations have kind: "system" and a container resource ref. Their message views include the same viewer-specific reactions as ordinary chat messages. Reaction live events carry a refresh hint on the item thread key; clients read the current reactions over HTTP. Item threads carry resource: { ref, title, context?, status?, done?, permission, container?, href?, typeTitle, icon, color? }. The reader's standard view supplies the title, localized context and status, completion flag, permission, and container { ref, title }. The catalog supplies the type title, icon and optional color. Icons must match ^ti ti-[a-z0-9-]+$ (at most 80 characters); invalid manifest icons use the default discussion icon. The reader’s optional open link supplies href; without it the header hides its open controls. Chat does not interpret domain fields or resolve status labels.

Chat no longer exposes separate assignees, deadlines, or exact event start and end times. A provider can include those details in its localized context line. Chat displays no numeric container membership count because the standard view does not supply one.

GET /system-chats accepts cursors, a JSON object keyed by container type. Responses contain { items, cursors, skipped }. Items include ref, name, typeTitle, icon, optional color, and nullable conversation. Send the returned cursors unchanged for the next page. An empty cursor means that provider is exhausted; position objects also preserve work for skipped providers. skipped identifies providers with unavailable lists, container readers or item summaries. Each unfinished position is an object with an optional provider cursor (at most 2,048 characters) and an optional container-page offset (0–100). For example, { "inventory.collection": { "cursor": "next-page" } } resumes a provider; { "inventory.collection": "" } marks it exhausted. The query is decoded once, without nested JSON cursor strings. The map accepts at most 1,500 keys (60 catalog pages of 25 providers); the serialized query is bounded by those limits and resource type lengths. Unvisited providers lead the next round, so a large or unavailable provider cannot starve others. Refresh to restart discovery after the directory changes.

Discovery, lists, readers and summaries share a maximum of 60 capability calls, 4 MiB of returned envelopes, six parallel readers and a ten-second deadline. Each round divides available calls among the remaining providers and reserves reader and summary work. Providers left unvisited lead the next round. Summary counts can be partial; follow summaryCursor to continue them.

Opening requires container read access. Creating an item thread and sending messages require container write access. The item reader must also authorize the caller, and its parent must match the conversation. Retrying either open operation returns the same object. Creating an item thread emits no thread_created message. Omit thread or pass null when sending to General.

System chats reject manual thread creation, renaming, archiving, chat metadata edits, and membership changes. Change container permissions in its owning app. Chat does not enumerate that app's members or copy their grants.

Unread messages begin at each participant's first open, or immediately before their first incoming mention or reply. Reopening preserves that position. Earlier messages cannot be marked unread. Mentions and replies appear in the same For you feed as ordinary chats, subject to current container and item access.

Mention targets use principal UUIDs and need only be active eligible people or service accounts. They need no Chat grant or previous resource read. Candidates are previous conversation authors, excluding the caller. Mentioning someone does not grant access: the feed, count, message reads, and content-bearing live events still use that reader's own container and item authority. The personal for-you live hint carries no content or resource identity.

Search the chat list

GET /api/chat/search?q=IT returns { items, systemUnavailable?: true }. The required q is trimmed and must contain 1–100 characters; invalid input returns HTTP 400. Matching is literal and case-insensitive, including %, _, and punctuation. The response contains at most eight hits, ordered by exact name, name prefix, word prefix, then other substring matches. Recent activity breaks ties. Archived conversations follow all active results; archived threads are excluded.

Hit Fields
Chat or subscribed channel { kind: "chat", conversation }, using the existing conversation-list summary. Generated titles use the caller's sidebar names and locale.
Named thread or item thread { kind: "thread", conversation: { id, title, kind }, thread: { id, name, resource? } }. The optional item resource contains { icon, done? }.
Discussion container { kind: "system", ref, name, icon, conversation }. The container ref is declared by its provider; conversation is null until its chat exists.

Ordinary results include only chats where the caller is a member, including subscribed channels. Discovery access alone does not add a result. Search covers ordinary titles and named threads before applying the result limit. It uses PostgreSQL and the existing membership indexes, with at most four member names used to derive each unnamed chat's title.

Container discovery sends query: q and limit: 8 to the provider's container query. A second page of up to eight containers, without the text filter, scopes item discovery. Item discovery inspects up to 60 recently active item threads in those caller-listed containers. Their stored names are snapshots: matching uses each reader's current name. Container and item readers must both allow the caller, and the item's current container must still match the conversation. The 60-second decision cache applies. Search shares the same 60-call, 4-MiB and ten-second request budget. Budget exhaustion keeps available results and sets systemUnavailable: true. These candidate bounds mean an older item thread or a container beyond its owner's first matching page can be absent from search.

When item threads remain, system-chat summaries include summaryCursor: "": search spends its item-reader budget on matching and leaves summary continuation to the existing conversation route. Empty thread directories are complete and return summaryCursor: null. Container activity uses General messages and readable item candidates. Open a container with no conversation through the existing POST /system-chats/:resourceType/:resourceId/open route.

If a provider fails, search retains ordinary hits and successful results from other providers, and sets systemUnavailable: true. Show the partial failure separately and retry the same query. An explicit denial omits the resource. An empty successful response is { "items": [] }. The standard Chat list request limiter applies (HTTP 429); disconnects cancel search queries and further reader work.

A search that reaches the shared transport budget returns incomplete: true with its authorized results and a Retry notice. systemUnavailable identifies an actual provider failure; a cold cache reaching its budget is not an outage.

The browser client in frontend/search-api.ts exports createSearchApi() with search(q, signal?) and the SearchHit, SearchResult, and SearchApi types. It validates both the query and response and preserves server error messages and codes.

Read item events

GET /conversations/:conversation/events reads a system conversation's item changes with the current caller. Ordinary conversations return 404. Pass thread=all (the default) for the container or a thread ID for that item. The response is { items, nextCursor, unavailable? }. Each row has id, at (the bucket's last occurrence), icon, a final localized sentence, count, and thread, plus an optional href supplied by the owning app. href is a same-origin path beginning with a single /, at most 2,048 characters, with no backslashes or control characters. Absolute and protocol-relative URLs are rejected. An existing thread is { id, name } with its current title; otherwise { ref } can be passed to threads/for-resource.

Rows are newest first. A page can be shorter to fit the capability response budget; follow its cursor even when fewer rows than requested arrive. Actor labels are limited to 1,000 characters. limit defaults to 50 and accepts 1–100. Use exclusive ISO datetime bounds after and before to match a message window. Follow a non-null nextCursor by passing cursor with the same selection and bounds, including when the page is empty. IDs identify owner activity buckets; count reports the number of occurrences already recorded in that bucket. Clients should replace an existing row with the same ID when refreshing it.

Events come from the provider's optional activity query. They never advance unread state, become a conversation preview, trigger a notification, or enter Chat's live payloads. The provider formats sentences in the caller's locale. Chat sends the request time zone, container ID and optional item ID through the standard activity input. It checks each item's current reader before returning an event. An owner outage or malformed activity response returns HTTP 200 with { items: [], nextCursor, unavailable: true }; retry the same request to recover. Item reader failures preserve the incoming cursor. Denied conversation, thread, or activity access returns 404. Providers without activity return an empty complete page.

Follow pages

Container pages use the cursors map described above. A catalog failure returns PROVIDER_UNAVAILABLE (HTTP 500). A list failure or an exhausted budget preserves that provider's position and adds its type to skipped, while other providers can still contribute results. An incomplete summary is marked on its conversation; a broken item reader never rewinds the provider directory. An individual container with a failed or malformed reader is omitted and its type is reported in skipped; the directory continues past it so one broken resource cannot block later containers. Refresh discovery to retry omitted containers. Retry unfinished provider pages with the returned map.

A system thread page inspects at most 60 candidates by latest activity, with the thread ID breaking ties. Visibility, search, and state filters apply within that page. Follow a non-null nextCursor even when items is empty. Ordinary chats retain their existing activity and archive ordering. short=true selects the four recent candidate threads, unread candidates, and optional selected thread within the candidate page. The selected thread and recent candidates take priority. Candidate selection precedes item authorization and unread-state calculation; short refreshes never calculate unread state across the whole directory. A partial short selection returns nextCursor: ""; open the directory to continue from its first page. It never supplies an aggregate count. Automatic refresh reads this one short page, while explicit directory browsing follows cursors. Live directory refresh re-reads exactly the number of pages the reader already loaded, sequentially and with cancellation, preserving existing row nodes.

Conversation summaries count only authorized item threads in the current candidate page. A non-null summaryCursor means the counts and preview are partial. Continue at GET /conversations/:conversation?summaryCursor=..., including when the cursor is the empty string (no item candidates inspected yet). Add each page's threads and unread, capping unread at 100; General is counted once, on the first inspected page. Select the newest lastMessage across pages. Counts describe each request's current authorized view; changes between requests can change the totals. The sidebar and automatic conversation refresh use one bounded summary page and retain its partial marker; they do not exhaust summary continuations before rendering. While a summary is partial, the row and section show … for an unknown count or n+ for a lower bound. An incomplete zero does not mark the section as read, including for the open chat.

Only actual remote calls and returned envelopes consume the shared request budget; cache hits are free. Candidate traversal has its own bound: a summary inspects at most 60 candidates, including hidden ones. Budget exhaustion returns a partial page and keeps continuation before the first uninspected candidate, even when nothing is returned. It does not fail the snapshot or SSR. Thread directories, conversation summaries, message pages, change feeds, shared lists and For you pages carry incomplete: true when a reader is unavailable or the request budget stops work. An unavailable item is hidden while readable siblings remain available. The directory and conversation show a notice with Retry; retry the read to check omitted items again. Cancellation and the shared deadline stop outstanding reads.

General and an individually selected thread authorize only the container and, for the latter, that item. Their snapshots contain only the selected thread's counts and list; summaryCursor: "" directs callers to the full summary read. The combined snapshot uses the bounded short thread selection. Aggregate history authorizes at most 50 candidate messages per request, change feeds 200, and shared history 32. These reads never enumerate all threads. Follow history from/to, change-feed rev/more, and shared nextCursor even when the returned items are empty: they advance past hidden candidates. When the shared or For you page reaches its transport budget, incomplete is true and nextCursor preserves the incoming cursor (or "" for the first page). Retry that page; the interface keeps readable rows and offers Retry instead of continuing past unchecked items.

POST /conversations/read-all inspects at most 50 candidate conversations and 100 item threads across them. A completed page returns marked, more, and, for system conversations, nextCursor. Repeat with { "confirmed": true, "cursor": "<nextCursor>" } while more is true, including when marked is zero. The cursor may continue within the same conversation. An incomplete unconfirmed preview returns outcome: "confirm" without changing read state. An ordinary-chat page without a cursor retains its existing repeat behavior.

For POST /conversations/:conversation/read with thread: "all", repeat the same sequence with the returned non-null summaryCursor to advance remaining item pages. General and selected-thread read receipts describe their selection; read the conversation summary separately for aggregate counts. System receipts are applied from authorized HTTP responses because they have no personal live event. The client follows All-thread read cursors sequentially at the same sequence and selection, rejects repeated cursors, and cancels pending work when the selection closes. Failed writes retain the existing read error and retry path.

Access and runtime dependencies

Chat uses the public capability transport to Core with the requesting person's cookie or account credential. Core exchanges that credential for a restricted invocation; Chat sends credentials only to Core. The standard view supplies read, write, or admin permission. Service accounts and agents retain their existing scope and resource caps.

Valkey stores decisions for 60 seconds, separately for each principal, credential scope, resource, and display locale. Live authorization uses a separate locale-independent permission projection for each resource. A revoked permission can remain usable during that interval. Reader failure or unavailable Valkey denies access when no valid decision exists. Chat's own grants never replace an unavailable reader. No credential is stored in the cache or a background job.

Live item messages and summaries use separate thread keys. Delivery requires cached authorization for both the container and item. Creating an item thread also emits the existing { "type": "threads", "conversationId": "…" } refresh hint on the container's conversation and list keys. It carries no item ID, title, resource ref, or message. Existing conversation viewers use it to refresh the thread list over authorized HTTP, even with no cached item decision. A viewer denied by the item reader receives no item metadata or message payload. New subscriptions discover the keys authorized by that HTTP refresh. The platform's live viewer carries no credential: a missing or expired decision denies delivery until an authenticated HTTP read refreshes it. Clients must refresh a snapshot when the live subscription requests resynchronization. The platform's existing short live-authorization cache still applies. System read receipts are not published on personal channels, which cannot check each receiver's container and item permissions; HTTP read responses contain the authorized read state.

The startup migration adds conversation kind and resource columns, thread resource columns, and unique indexes. Existing conversations remain ordinary chats. Manual thread names remain unique within a conversation; distinct items may have the same title. Chat's member reconciliation excludes system chats, whose authority belongs to the owning application.

The OpenAPI document at /api/chat/openapi.json describes request and response schemas. No additional environment variable or app workload credential is required for this request-driven slice.

Thread discovery hints invalidate both the sidebar and the open directory. Both sidebar discovery and the open conversation perform an authorized short thread read before renewing their existing live subscription. Summary reads alone cannot warm new item decisions beyond their ID-ordered candidate page. Renewal starts at the earliest relevant hint in the received batch. Events committed during discovery replay after renewal, even if a later known-key event in that batch has already advanced the live watermark.

“Jump to latest” preserves authorized live arrivals newer than its snapshot. Access reconciliation instead replaces the authorized window and supersedes pending reads, so a delayed response cannot restore removed content.

Automatic read failures appear with a retry in the existing composer hint or read-only status line, keeping the message viewport's layout stable.

Personal lists alongside system chats

Personal pins, archive, All/Unread filters, and saved collapsed sections apply to ordinary chats and subscribed channels. SidebarPreferencesSchema.collapsed accepts pinned, chats, and channels. System chats stay in the separate Discussions section; personal mutation requests for them return 403. The ordinary conversation list excludes system chats in every list mode.

Unread filtering uses each thread's read state before pagination. The workspace tracks unread-gap recovery per selected thread, including All threads in ordinary and system chats. Recovery in /app/chat/panel preserves that route base and the current list and thread selection. System-chat addresses use listFilter=all|unread for the personal list; filter=open|done|unread remains the independent system-thread directory filter. All is the default personal list mode and is omitted from generated system-chat URLs.

Notification choices and followed discussions

Conversation responses (detail, lists and snapshots) include notifications and mentionsUnread. notifications is null without a participant row, including discovery results before participation. With a row it contains:

json
{
  "level": "mentions",
  "choice": null,
  "until": null,
  "defaultLevel": "mentions",
  "following": { "count": 0, "first": null }
}

level is the effective all, mentions or none level. choice: null means the default. System chats default to mentions. In discoverable channels, participants with only read permission default to all. Other chats default to all through 20 members and mentions from 21. An explicit choice survives changes to membership and permissions. following is null for ordinary chats.

mentionsUnread counts unread mentions and replies to the participant in this conversation, including when muted. A message that both mentions and replies counts once. It uses the same recipient sources, read boundaries, access checks and cap of 99 as For you. Counts scan at most five pages of 100 candidates; inaccessible candidates can make the displayed count lower.

All paths below start with /api/chat and require current read access and a participant row. Unreadable resources return 404; a readable chat without participation returns 403. Mutations also require write credential scope. Service accounts can store these preferences like users.

Request Input or result
GET /conversations/:conversation/notifications The notifications object.
PUT /conversations/:conversation/notifications { "level": "default" }, { "level": "all" }, { "level": "mentions" }, or { "level": "none", "for": "1h" }; returns the new object.
PUT /conversations/:conversation/threads/:thread/follow Follow an item discussion; returns { "following": true }.
DELETE /conversations/:conversation/threads/:thread/follow Unfollow it; returns { "following": false }.

Only none accepts for: 1h, 8h, 1w, or forever. The server computes expiry; callers cannot supply until. Once expired, a mute reads as the current default with choice: null and until: null. The next preference write replaces the stored choice and clears or replaces its expiry. No scheduled cleanup is needed.

Only item threads in system chats support following. Posting a new message in one follows it in the same transaction; retrying an existing message, reacting, or editing does not. Thread projections include following for item threads. Follow and unfollow are idempotent. Removing a participant or deleting a thread removes its follows. System chats have no leave route; access and participant cleanup remain owned by the existing resource lifecycle.

The conversation's following summary counts its stored follows and returns the most recently followed thread as first: { id, name }. If that thread is no longer readable, first is null; its name and ID are not disclosed.

Preference and follow changes enqueue a content-free { "type": "notifications" } hint on the subject's personal live channel, in the same transaction. Other tabs refetch their authorized conversation and thread state. No item title or conversation ID travels on this hint.

The Chat CLI module exposes:

sh
cld chat notify <chat>
cld chat notify <chat> all|mentions|default
cld chat notify <chat> off [--for 1h|8h|1w]
cld chat follow <chat> <thread>
cld chat unfollow <chat> <thread>

Omitting --for keeps a mute until changed. All commands support --json and --jsonl. Capabilities chat.notifications.read, chat.notifications.set and chat.thread.follow use these same services. The read Query accepts { conversation }; the set Action accepts { conversation, notifications } with the PUT body above; the follow Action accepts { conversation, thread, following }. Results contain the same notification object or { following } in the capability data envelope.

Resource checks inside these capabilities use the parent invocation's query authority and bypass the HTTP decision cache. They do not forward the person's cookie or token.

Followed item discussions, mentions, and replies can send push notifications when the provider's reader check allows the recipient. Chat checks the item against this conversation's expected container, using its automatic workload credential through Core. Failed checks omit counts and retry delivery; missing setup suppresses push and appears in Chat administration. See Chat notifications for rollout, timing, count budgets and troubleshooting.

Edit or delete a message

PATCH /conversations/:conversation/messages/:message accepts clientId, revision (the current message contentRevision, initially zero), and body. DELETE on that path accepts clientId and revision. Both require the author's current Write or Manage access and a write-capable credential, within ten minutes of sending. Archived threads cannot be changed. They apply to ordinary chats and item threads.

Use a fresh clientId per edit or deletion and retain it with the same input when retrying a lost response. Receipts remain replayable for 24 hours, then bounded maintenance batches remove them. Reactions never change the content revision. A stale revision returns MESSAGE_CHANGED; an expired window returns MESSAGE_MUTATION_EXPIRED. Both are HTTP 409. Successful message views include editedAt and deletedAt. Deletion clears text and attachments while preserving the message's position and a deleted-message marker for replies. The revision stream and live invalidation refresh other tabs without counting the edit as a new message.

Item reader container context

The provider's standard view.container supplies the container's ref and title. Chat reads container and item permissions separately. A consuming capability uses a fresh nested reader under the incoming authority; display metadata never grants access to other items or replaces credential scope and resource binding checks.

Browser loading

Chat loads forms, search, info and the channel and thread views when they open. Those destinations initially show the same loading notice on the server and in the browser, then use the authorized snapshot already supplied by the server. A failed code download leaves a visible Try again action in that destination. The conversation and composer remain in the initial island bundle. Browser validation of messages and live responses remains enabled.

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.