Chat¶
Chat is an organization-scoped conversation attached to an arbitrary resource. It is the surface where reviewers and operators talk about a resource — and the channel through which the platform's agents converse with users: a bot message is simply a message with no author.
Properties¶
| Field | Type | Description |
|---|---|---|
| organization_id | Organization relation | Owning organization; every access check is org-scoped |
| mode | enum: RESOURCE, DIRECT |
How access is authorized (see below) |
| context_type | enum: SHOOTING, SET_DESIGN, ORG_ASSISTANT |
Which kind of resource the chat is attached to |
| context_id | UUID | The attached resource's id — polymorphic, no foreign key |
| title | string, optional | Display title |
One live chat exists per resource: a partial unique index on (context_type, context_id) scoped to deleted_at IS NULL, so a soft-deleted chat never blocks re-creating one.
Context types¶
| Context type | context_id points at |
Backing entity |
|---|---|---|
SHOOTING |
a shooting row | Shooting |
SET_DESIGN |
a set-design row | SetDesign — org-scoped only; personal designs (null organization) are rejected |
ORG_ASSISTANT |
the organization itself (context_id == organization_id) |
none — one global assistant chat per organization |
ORG_ASSISTANT is the deliberate special case: its "resource" is the organization, which is what gives each org a single, always-available assistant conversation.
Modes¶
RESOURCE(default) — access follows the resource: any member of the chat's organization can read and post. The participant roster is unused.DIRECT— a private conversation authorized by an explicitChatParticipantroster. The model and endpoints exist; no product surface uses it yet.
Data model¶
erDiagram
Chat ||--o{ ChatMessage : contains
Chat ||--o{ ChatParticipant : "roster (DIRECT only)"
ChatMessage ||--o{ MediaResourceAttachment : attachments
ChatMessage }o--o| ChatMessage : reply_to
Chat {
uuid id PK
uuid organization_id FK
enum mode "RESOURCE | DIRECT"
enum context_type "SHOOTING | SET_DESIGN | ORG_ASSISTANT"
uuid context_id "polymorphic, no FK"
string title
datetime deleted_at
}
ChatMessage {
uuid id PK
uuid chat_id FK
uuid owner_id FK "NULL = system/bot"
string text "max 8192"
bool is_private "inverted naming, see below"
uuid reply_to_id FK "ON DELETE SET NULL"
json options "pressable options payload"
datetime deleted_at
}
ChatParticipant {
uuid id PK
uuid chat_id FK
uuid user_id FK
}
Messages participate in the MediaResource system through the attachments slot (MediaEntityType.CHAT_MESSAGE), so an image-only message is valid.
Visibility and roles¶
is_private reads backwards: is_private = true means shared / reviewer-visible; false is an operator-internal note. Everything else follows from that.
| Rule | Behavior |
|---|---|
| Reviewers see only the private subset | Message listing, batch counts, the action-needed scan and the WebSocket fan-out all filter on is_private for a REVIEWER viewer |
| A reviewer cannot write an internal note | Posting coerces is_private = true for any reviewer author, whatever the request says |
| Authors are masked for reviewers | A reviewer never learns who wrote someone else's message — owner / owner_id are nulled per viewer (REST) and per recipient (WebSocket). The author always sees their own identity |
| Typing is anonymized | A typing signal delivered to a reviewer carries no user_id; the UI shows a generic "someone is writing…" |
| A reviewer comment reopens the resource | Posting as a reviewer reopens the backing resource (a published set design returns to draft) in the same transaction as the message |
Message rules¶
| Rule | Value |
|---|---|
| Text | Max 8192 characters; text or at least one attachment is required |
| Reply-to | Flat, one level; the parent must be a live message in the same chat. ON DELETE SET NULL — a reply outlives its parent |
| Reply preview | Caller-aware: full text when the viewer may see the parent, is_restricted when not, is_unavailable when the parent is soft-deleted |
| Delete window | Own message, within 5 minutes of posting |
| Pagination | Keyset over (created_at, id); the returned page is ordered oldest→newest with an opaque next_cursor for older history |
Pressable options¶
A bot message can offer pressable items instead of asking for free text — for example "which two subjects should we use?".
Storage is a minimal, normalized JSON payload on the message (direction, kind, select, resolved, selected_values, values) holding opaque ids only — no labels, no URLs. The public shape is hydrated at read time.
| Field | Meaning |
|---|---|
direction |
offer — a bot message proposing items; response — an internal carrier on a user message recording the choice (never exposed publicly) |
kind |
option.subject today; new kinds slot in as additional payload types |
select |
single or multi, enforced server-side |
resolved |
Locked once answered — a second answer is rejected with 409 |
A user answers either by pressing (a dedicated respond endpoint validates, locks the offer and emits a live update) or by typing the names (the agent's tool applies the selection and the lock is written inside the same turn transaction). Both paths are idempotent.
API surface¶
All endpoints live under /chats; write and list endpoints are organization-gated.
| Method + path | Purpose |
|---|---|
POST /contexts/{context_type}/{context_id} |
Get-or-create the chat for a resource (201 created / 200 existing). POST because it may create a row |
GET / · GET /{chat_id} |
List / read chats |
GET /contexts/{context_type}/messages/count |
Batch message counts for up to 200 contexts |
GET /contexts/action-needed |
Chats awaiting the caller's reply (see below) |
POST /{chat_id}/messages · GET /{chat_id}/messages · DELETE /{chat_id}/messages/{message_id} |
Post, list (keyset), delete |
POST /{chat_id}/messages/{message_id}/options/respond |
Press an options block (reviewer-only) |
POST /{chat_id}/participants · DELETE /{chat_id}/participants/{user_id} |
DIRECT roster management |
Action needed¶
A context "needs action" when the last message the caller can see is at least 2 minutes old — there is no maximum age — and was written by a different role than the caller, so a thread where the caller's own side spoke last is skipped. A bot/system message counts only for REVIEWER callers. Contexts whose backing resource is already completed (a published set design) are dropped.
The response carries a grand total (the global badge) plus a per-context-type breakdown with the individual resource ids, so a page can mark each card that needs a reply.
Real-time¶
Chat rides the platform WebSocket connection; events fan out through Redis pub/sub.
| Direction | Frame / event | Notes |
|---|---|---|
| client → server | chat.subscribe |
Authorizes the connection for a chat, then replays the recent backlog to it |
| client → server | chat.unsubscribe, chat.typing |
Typing is ignored unless subscribed |
| server → client | chat.message_created, chat.message_updated |
Carry the full message; visibility-filtered per recipient — an operator-internal message never reaches a reviewer connection, and authors are masked per recipient |
| server → client | chat.message_deleted |
Ids only, deliberately not visibility-filtered — a tombstone leaks nothing |
| server → client | chat.typing |
Ephemeral, never stored. is_bot = true (with no user id) is the agent's "Assistant is writing…" signal |
| server → client | chat.subscribed, chat.subscribe_error |
Ack / rejection (not_found, forbidden, bad_request); the connection stays open |
Lifecycle hooks¶
The chat service notifies a neutral hook registry on two events — chat created (synchronous) and user message posted (asynchronous, after commit and fan-out). Dispatch is best-effort: a failing hook is logged and never fails the chat operation.
This registry is what lets the agent subsystem provision and drive a conversation without the chat code ever importing it — a one-way dependency with no import cycle. See Agent Conversation Flow.
Related documentation¶
- Agent Sessions — the agents that converse in these chats
- Agent Conversation Flow — how a message becomes a bot reply
- Chat UI patterns — the webapp implementation
- MediaResource Lifecycle — message attachments