Spectral Agent Chat Privacy
This page describes the privacy model for Spectral Agent conversations only. The three broadcast conversation states and the fork-on-reply mechanism apply to Spectral Agent customer-facing conversations; the World Agent and Operations Agent have separate conversation models documented in their own pages.
Spectral Agent conversations have three distinct privacy states.
The state of a conversations row is determined by the combination of
initiated_by, user_id, forked_from, and broadcast_key, and is
enforced both at the application layer (repo + router) and at the
database layer (RLS policies + partial unique indexes).
Three conversation states
Section titled “Three conversation states”1. Customer-initiated private chat
Section titled “1. Customer-initiated private chat”initiated_by = 'customer'user_id = <owner>forked_from = NULLbroadcast_key = NULLVisible and writable only to user_id. Acts like a ChatGPT session —
other workspace members cannot list, read, or write to these
conversations. This is the default shape for anything created via
POST /api/v1/workspaces/{id}/agent/chat without an explicit
conversation_id.
2. Broadcast template
Section titled “2. Broadcast template”initiated_by in ('agent','system')user_id IS NULLforked_from IS NULLbroadcast_key = '<stable-key>'A broadcast template is a read-only conversation starter visible to
every member of the owning workspace. The broadcast generator upserts
these rows by (workspace_id, broadcast_key) so re-running it is
idempotent — the generator never creates duplicates and never overwrites
messages on existing rows.
Workspace members see broadcasts in their conversation list alongside their own private chats. They cannot write to the broadcast directly; any attempt to reply triggers the fork-on-reply flow below.
3. Forked private branch
Section titled “3. Forked private branch”initiated_by in ('agent','system')user_id = <replying user>forked_from = <broadcast id>broadcast_key = NULLWhen a user first replies to a broadcast template, the server creates a
new conversations row owned by that user, copies the broadcast’s
messages as seed history, and appends the user’s first message. The
original broadcast remains intact for other workspace members. Each
user who engages with the same broadcast gets their own distinct fork;
concurrent first-replies from the same user are collapsed to a single
fork via a partial unique index on (forked_from, user_id).
API surface
Section titled “API surface”All routes live under /api/v1/workspaces/{workspace_id}/agent/ and
require read:workspace.
POST /api/v1/workspaces/{id}/agent/chat
Section titled “POST /api/v1/workspaces/{id}/agent/chat”Sends a user message to the agent. Behavior depends on the target:
| Target | Result |
|---|---|
No conversation_id | Creates or resumes the caller’s own private conversation |
conversation_id owned by caller | Appends the message to the existing conversation |
conversation_id is a broadcast template | Forks into a new per-user branch and returns the fork id |
conversation_id owned by another user | 404 (existence is not leaked) |
GET /api/v1/workspaces/{id}/agent/conversations
Section titled “GET /api/v1/workspaces/{id}/agent/conversations”Lists conversations visible to the caller:
- All of the caller’s own private conversations (customer chats and forked branches they’ve engaged with).
- All un-engaged broadcast templates in the workspace.
Never includes another member’s private conversations — even workspace admins must use a different surface to audit per-user chats.
GET /api/v1/workspaces/{id}/agent/conversations/{conversation_id}/messages
Section titled “GET /api/v1/workspaces/{id}/agent/conversations/{conversation_id}/messages”Returns 404 if the caller does not own the conversation and it is not a
broadcast template. The router enforces this with a pre-check against
get_conversation(conv_id, workspace_id, user_id) before ever issuing
the messages query, so a wrong-user probe cannot confirm whether a
conversation id exists.
Database enforcement
Section titled “Database enforcement”The conversations table carries check constraints that prevent
invalid state combinations:
conversations_customer_has_user— customer-initiated rows must have a non-nulluser_id.conversations_broadcast_has_no_user— rows withbroadcast_keyset must haveuser_id IS NULLandinitiated_by in ('agent','system').conversations_fork_has_user— forked rows must carry bothforked_fromanduser_id.
RLS policies enforce per-user visibility:
conversations_select— members see their own conversations plus un-engaged broadcasts in their workspace. Workspace admins and account owners retain full read visibility for moderation.conversations_insert_customer_or_fork— authenticated users can only insert rows whereuser_id = auth.uid(). Broadcast creation (user_id IS NULL) requires the service role — the application layer creates broadcasts viaAsyncSupabaseAgentConversationRepository.create_broadcast, which runs inside a worker/service-role connection.
The is_conversation_member(conv_id) helper used by the
conversation_messages policies extends to broadcasts — any workspace
member is treated as a member of every un-engaged broadcast in that
workspace, so the per-message policies continue to work uniformly for
both private and broadcast states.
When to use broadcasts
Section titled “When to use broadcasts”Broadcasts are appropriate when Spectral wants to push the same conversation starter to every workspace member — for example:
- Daily/weekly digests of scan results.
- Notifications about completed change sets requiring review.
- System announcements scoped to a single workspace.
If the content differs per-user (e.g. “your scan completed”), create per-user conversations directly rather than a broadcast. Broadcasts are a sharing mechanism, not a templating engine.
What’s next
Section titled “What’s next”- Operations App overview — the next cluster in the spine. The Operations app hosts the operator-side surface, where authoring, distillation, and publication workflows live alongside the Operations Agent.
Further reading
Section titled “Further reading”- Agent Architecture — the three-agent topology this page sits inside; this page is the Spectral-Agent-only privacy specification.
- Access Control — the RLS doctrine the per-user conversation policies on this page extend.
- Notifications — channel adapters that interact with
conversations (the in-app
InAppAdapteropens/extends conversations on broadcast push).