> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# The team chat model: channels, huddles, presence, reactions, and attachments

> How Orbit models internal team chat — channels and direct-message threads, the ephemeral objects layered on messages (reactions, presence, typing), huddles as per-channel voice rooms, the notification category team chat lands in, attachment handling rules, and when to reach for team chat versus the customer-facing Inbox.

# 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](/guides/team-chat); this page is the model the [Team Chat API](/api-reference/team-chat) 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 a `member` unless the owner promotes them to `admin`. 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](#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](/concepts/agent-presence-lifecycle).

## Notifications interplay

Team chat activity becomes attention through the same pipeline every other workspace event uses — the [notification model](/concepts/notification-model). When someone posts a channel message or sends you a DM, every other member gets a `team_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_message` mints 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.

The channel-level unread counter (`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](/concepts/conversations) defines the thread.

Membership and participation enforce this at the API layer: a non-member's channel read returns nothing, search drops a channel out of results the moment you are removed from it, and a DM endpoint returns `403` for a non-participant.

## Attachment handling

Attachments follow the shared upload-then-reference flow:

1. 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.
2. Reference the id when posting the message; the message row persists `{ file_id, filename, content_type, size }` and nothing else.
3. Readers mint a fresh short-lived download URL on demand through the files endpoint — no expiring signed URL is stored, so attachments never rot.

The rule set:

* **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 `url` is 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](/guides/team-chat) — the operator workflows (handoffs, escalations, DMs) built on this model.
* [Team Chat API reference](/api-reference/team-chat) — the endpoint and field contract.
* [Agent presence and aux-code lifecycle](/concepts/agent-presence-lifecycle) — the dispatch-eligibility state machine, deliberately separate from collaboration presence.
* [The notification model](/concepts/notification-model) — the category, bell-row, and digest pipeline `team_chat_message` rides.
* [Conversations: the omnichannel thread model](/concepts/conversations) — the customer-facing thread team chat never touches.
