The team chat model
Team chat is Orbit’s internal collaboration layer — the object graph your operators coordinate on while they work customer conversations elsewhere. This page defines that graph: the durable objects (channels, members, direct-message threads, messages), the deliberately ephemeral objects layered on top (reactions, presence, typing, and huddle rooms), and the rules that decide what lives where. The operator walkthrough is in the team chat guide; this page is the model the Team Chat API endpoints hang off. Separating durable from ephemeral is the design spine of the whole surface. Channels and messages are persisted in your workspace and hold the conversation you search; presence, typing, reactions, and huddles are cache-resident by design, because making a database row out of “online” that must expire on disconnect is the classic presence-in-a-table anti-pattern.Team chat objects
Five object families carry the surface:- Channels and members. A channel is a named room with a membership list. The creator becomes its
owner; everyone else joins as amemberunless the owner promotes them toadmin. Every channel read, write, search, and typing call is gated on membership — membership is the authorization boundary, so there is no separate revocation path. - Direct-message threads. A DM thread is a two-participant channel — only the two participants can read it, post to it, or delete it. An admin cannot browse someone else’s DM.
- Messages. Channel messages, in-thread replies, and DM messages are persisted rows. Replies hang under a thread root; edits and deletes are scoped to your own messages; owners/admins can moderate any member’s channel message.
- Reactions. Emoji reactions are per-(message, emoji, user) membership state — high-churn toggles with no natural owner row — so they live in ephemeral key-value storage as individual add/remove operations rather than in a message column. A message may carry up to 20 distinct emojis, and the summary returns a capped sample of reactor ids per emoji while keeping exact counts.
- Attachments. A file you upload once and then reference in a message. Upload returns a platform
file_id; the message row stores just the metadata (file_id,filename,content_type,size). Download URLs are minted on demand, so nothing expiring is ever persisted. The full rule set is in Attachment handling below.
Presence lifecycle and huddles
Presence answers “can I expect a reply right now?” Reads see three states —online and away as reported by the member, and offline as derived from absence. A heartbeat refreshes an online window per member; when heartbeats stop (a closed tab, a dropped network), the member ages out of that window and reads as offline to their teammates. A graceful sign-out collapses that window to offline at once. Last-seen timestamps survive going offline, so “last seen 40 minutes ago” still resolves long after the online window lapsed. Two rules make this dependable: presence is never a persisted status (that would require a background sweep to clear rows stranded by a hard tab-close), and a member who is merely in a backgrounded browser tab must not flip offline — the online window is deliberately sized to outlast the browser’s throttled timer for hidden tabs.
A huddle is the live extension of presence: an ad-hoc voice room scoped to exactly one channel. Any member can start one, other channel members drop in, and the room ends automatically when the last participant leaves — there is no separate “end meeting” step. The room identity is deterministic per channel, so start and join are the same code path whether you are first or fiftieth to arrive, and the media plane self-reclaims an abandoned room without a cleanup job.
This collaboration presence is a different object from the presence an agent scheduler consults. Team-chat presence is an interest signal (“expect a reply now”); it never feeds dispatch, capacity, or eligibility. For the five-state machine (available / busy / wrapup / paused / offline) that voice routing and agent eligibility read, see Agent presence and aux-code lifecycle.
Notifications interplay
Team chat activity becomes attention through the same pipeline every other workspace event uses — the notification model. When someone posts a channel message or sends you a DM, every other member gets ateam_chat_message event, which lands on the messaging category of your notification preferences — the same category that carries inbox events such as inbox_new_message and inbox_mention.
Two of the notification model’s special classes apply directly:
- In-app-only.
team_chat_messagemints its Notification Center row unconditionally and never leaves the dashboard as an email — the bell is the surface, so the row is always written rather than gated on your outbound toggles. - Registry before reach. The persisted bell row is the source of truth your unread badge counts; the notification preferences page just shapes outbound reach, never whether the row exists.
unread_count + last_message_at returned on channel listing) is a separate, per-channel read cursor you advance with the read call — it complements, rather than duplicates, the notification pipeline. For a file-only message the notification preview falls back to a file-count summary (“Sent 2 attachments”) so it never renders blank.
Presence and typing deliberately emit no notification kinds — they are attention signals rendered live on the existing per-tenant realtime feed, not events worth a bell row.
When to use team chat vs inbox
Choose the surface by audience, and the split is absolute:- Team chat is operator-to-operator. Nothing posted in a channel or DM reaches a customer, and no outbound customer message is touched by any team-chat endpoint. Use it for on-call handoffs, escalation chatter that references (but never modifies) an Inbox conversation, and live huddles while triaging.
- Inbox is the customer-facing conversation surface. Every message a customer can read lives there; the conversations model defines the thread.
403 for a non-participant.
Attachment handling
Attachments follow the shared upload-then-reference flow:- Upload the bytes once; you get back a platform
file_id plus display metadata. Any authenticated member of your workspace may upload, so a rank-and-file channel member can share a file. - Reference the id when posting the message; the message row persists
{ file_id, filename, content_type, size }and nothing else. - Readers mint a fresh short-lived download URL on demand through the files endpoint — no expiring signed URL is stored, so attachments never rot.
- Size. Each file must fit the shared upload ceiling (25 MiB), and a message carries at most 10 attachments.
- Type. Images (JPEG, PNG, GIF, WebP), audio, video (MP4, 3GPP), PDF, Office documents, CSV, plain text, and ZIP archives. SVG is deliberately excluded — it is a stored-XSS vector.
- Integrity. The metadata schema rejects unexpected fields, so a client-supplied
urlis never persisted. - Tenant ownership. Upload limits and the allowed-type list are workspace-level rules; nothing routes through platform-global gates, and the metadata you persist is exactly what the upload route would have accepted.
Where this fits
- Team chat guide — the operator workflows (handoffs, escalations, DMs) built on this model.
- Team Chat API reference — the endpoint and field contract.
- Agent presence and aux-code lifecycle — the dispatch-eligibility state machine, deliberately separate from collaboration presence.
- The notification model — the category, bell-row, and digest pipeline
team_chat_messagerides. - Conversations: the omnichannel thread model — the customer-facing thread team chat never touches.