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

# WebSocket

> Real-time channels for log streaming, chat, events, and live updates.

Crewship pushes real-time updates — log streaming, chat token streams, and entity-change events — over a single token-authenticated WebSocket. Clients subscribe to typed channels (`workspace:{id}`, `session:{chatId}`, …); the server validates each subscription against database membership and fans state changes out to the subscribers. Most dashboard state arrives on the `workspace` channel, while a few high-fanout streams (chat, mission detail, files) use narrower channels.

<Note>
  Connecting requires a short-lived JWT from `GET /api/v1/ws-token` (itself session- or CLI-authenticated). The token is validated only during the handshake; the connection stays authenticated afterward.
</Note>

## Connection

```
ws://<host>:8080/ws?token=<jwt>
```

### Authentication Flow

1. Obtain a short-lived JWT token: `GET /api/v1/ws-token` (requires session or CLI auth)
2. Connect to the WebSocket endpoint with the token as a query parameter
3. The token is validated during the handshake using the same JWT validator as the REST API
4. After connection, the token is no longer needed -- the connection remains authenticated

The WebSocket server uses `golang.org/x/net/websocket` (not gorilla/websocket). Authentication is token-based. Inbound frames are capped at 64 KiB.

<Warning>
  The handshake validates the `Origin` header against the request `Host` (cross-site WebSocket hijacking protection). `localhost`/`127.0.0.1` origins are allowed outside production; in production (`CREWSHIP_ENV=production`) the origin host must match exactly.
</Warning>

***

## Message Format

All messages (client-to-server and server-to-client) are JSON.

### Client Messages

```typescript theme={null}
{
  type: "subscribe" | "unsubscribe" | "ping" | "send_message" | "cancel_message",
  channel?: string,
  payload?: any
}
```

### Server Messages

```typescript theme={null}
{
  type: string,
  channel?: string,
  payload: any
}
```

***

## Client Message Types

### subscribe

Subscribe to a channel for real-time events. Channel access is validated against workspace/crew membership in the database.

```json theme={null}
{
  "type": "subscribe",
  "channel": "workspace:ws_abc123"
}
```

If access is denied, the server responds with:

```json theme={null}
{
  "type": "error",
  "channel": "workspace:ws_abc123",
  "payload": { "error": "access denied" }
}
```

### unsubscribe

```json theme={null}
{
  "type": "unsubscribe",
  "channel": "workspace:ws_abc123"
}
```

### ping

Keep-alive ping. The server also sends periodic pings every 30 seconds.

```json theme={null}
{
  "type": "ping"
}
```

Server responds with:

```json theme={null}
{
  "type": "pong",
  "payload": null
}
```

### send\_message

Send a chat message to an agent session.

```json theme={null}
{
  "type": "send_message",
  "channel": "session:chat_abc123",
  "payload": {
    "session_id": "chat_abc123",
    "content": "Review the latest PR and suggest improvements"
  }
}
```

The server processes the message through the ChatHandler, which streams responses back on the same session channel.

### cancel\_message

Cancel an in-progress agent chat response.

```json theme={null}
{
  "type": "cancel_message",
  "payload": {
    "session_id": "chat_abc123"
  }
}
```

***

## Channels

Channels follow the format `type:id`. Access is validated against database membership when subscribing.

### Channel Types

| Channel     | Format                    | Access Rule                   | Description                     |
| ----------- | ------------------------- | ----------------------------- | ------------------------------- |
| `workspace` | `workspace:{workspaceId}` | Workspace member              | Workspace-wide events           |
| `crew`      | `crew:{crewId}`           | Member of crew's workspace    | Crew-specific events            |
| `agent`     | `agent:{agentId}`         | Member of agent's workspace   | Agent-specific events           |
| `session`   | `session:{chatId}`        | Member of chat's workspace    | Interactive chat session        |
| `mission`   | `mission:{missionId}`     | Member of mission's workspace | Mission status updates          |
| `keeper`    | `keeper:{workspaceId}`    | Workspace member              | Keeper credential access events |
| `files`     | `files:{crewId}`          | Member of crew's workspace    | File change events              |
| `providers` | `providers`               | Any authenticated user        | Provider status updates         |

***

## Server Event Types

Events are broadcast to channels when state changes occur. Below are the event
types by channel. Most state changes fan out on the **workspace** channel so a
single subscription drives the whole dashboard; a few high-fanout streams
(chat, mission detail, files) use narrower channels.

Many payloads carry only the entity `id` (and sometimes `identifier`/`status`)
as a refresh hint -- the client is expected to refetch the full resource over
REST. The shapes below reflect what the server actually sends.

### Workspace Channel (`workspace:{id}`)

#### Crews & agents

| Event Type               | Description                                 | Payload                                                    |
| ------------------------ | ------------------------------------------- | ---------------------------------------------------------- |
| `crew.created`           | Crew was created                            | `{ id, name, slug }`                                       |
| `crew.updated`           | Crew was updated                            | `{ id }`                                                   |
| `crew.deleted`           | Crew was deleted                            | `{ id }`                                                   |
| `agent.created`          | Agent was created                           | `{ id, ... }`                                              |
| `agent.updated`          | Agent was updated                           | `{ id }`                                                   |
| `agent.deleted`          | Agent was deleted                           | `{ id }`                                                   |
| `agent.hired`            | Agent was hired                             | `{ id, ... }`                                              |
| `agent.rehired`          | Agent was rehired                           | `{ id, ... }`                                              |
| `agent.hire_approved`    | Agent hire request approved                 | `{ id, ... }`                                              |
| `agent.skill_assigned`   | Skill assigned to agent                     | `{ id, ... }`                                              |
| `agent.skill_unassigned` | Skill removed from agent                    | `{ id, ... }`                                              |
| `agent.expired`          | Ephemeral agent reached TTL and was ghosted | `{ id, crew_id, name, expired_at }`                        |
| `agent.log`              | Agent log output                            | `{ ts, level, agent, agent_id, event, content, metadata }` |
| `agent.status`           | Agent run status change                     | `{ agent_id, status, ... }`                                |
| `run.started`            | An agent run started                        | `{ agent_id, ... }`                                        |

#### Missions, tasks & issues

| Event Type            | Description                          | Payload                                     |
| --------------------- | ------------------------------------ | ------------------------------------------- |
| `mission.created`     | Mission was created                  | `{ id, ... }`                               |
| `mission.updated`     | Mission was updated / status changed | `{ id, crew_id, title, status }`            |
| `task.updated`        | Mission task status changed          | `{ id, mission_id, status }`                |
| `confidence.low`      | A task's planning confidence is low  | `{ task_id, mission_id, level }`            |
| `approval.required`   | A task is awaiting human approval    | `{ task_id, mission_id }`                   |
| `approval.resolved`   | A pending approval was resolved      | `{ task_id, mission_id, ... }`              |
| `issue.created`       | Issue was created                    | `{ id }`                                    |
| `issue.updated`       | Issue was updated                    | `{ id }` (sometimes `identifier`, `status`) |
| `issue.deleted`       | Issue was deleted                    | `{ identifier }`                            |
| `issue.started`       | Issue execution started              | `{ id, identifier, status }`                |
| `issues.bulk_updated` | Multiple issues updated in bulk      | `{ count }`                                 |

#### Projects & milestones

| Event Type          | Description           | Payload              |
| ------------------- | --------------------- | -------------------- |
| `project.created`   | Project was created   | `{ id }`             |
| `project.updated`   | Project was updated   | `{ id }`             |
| `project.deleted`   | Project was deleted   | `{ id }`             |
| `milestone.created` | Milestone was created | `{ id, project_id }` |
| `milestone.updated` | Milestone was updated | `{ id, project_id }` |
| `milestone.deleted` | Milestone was deleted | `{ id, project_id }` |

#### Integrations & credentials

| Event Type            | Description                                        | Payload                     |
| --------------------- | -------------------------------------------------- | --------------------------- |
| `integration.created` | Integration was added                              | `{ id, ... }`               |
| `integration.updated` | Integration was changed                            | `{ id, ... }`               |
| `integration.deleted` | Integration was removed                            | `{ id, ... }`               |
| `credential.expired`  | An OAuth credential expired (token refresh failed) | `{ credential_id, reason }` |

#### Assignments, escalations & peers

| Event Type                  | Description                                | Payload                              |
| --------------------------- | ------------------------------------------ | ------------------------------------ |
| `assignment.updated`        | Agent-to-agent assignment changed          | `{ id, status, ... }`                |
| `assignment_queued`         | Assignment was queued for dispatch         | `{ ... }`                            |
| `assignment_unqueued`       | Assignment was removed from the queue      | `{ ... }`                            |
| `escalation.created`        | An escalation was raised                   | `{ id, crew_id, from_slug, reason }` |
| `escalation.resolved`       | An escalation was resolved                 | `{ id, ... }`                        |
| `peer_conversation.updated` | Peer (agent-to-agent) conversation changed | `{ ... }`                            |

#### Containers & provisioning

| Event Type            | Description                                    | Payload                                                                                                                          |
| --------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `container.stats`     | Live container resource metrics (\~5s cadence) | `{ container_id, crew_id, cpu_percent, memory_used, memory_limit, memory_percent, net_rx_bytes, net_tx_bytes, pids, timestamp }` |
| `provision.started`   | Crew container provisioning started            | `{ ... }`                                                                                                                        |
| `provision.progress`  | Provisioning progress update                   | `{ ... }`                                                                                                                        |
| `provision.completed` | Provisioning finished                          | `{ ... }`                                                                                                                        |
| `provision.failed`    | Provisioning failed                            | `{ ... }`                                                                                                                        |
| `port_expose.created` | A container port was exposed                   | `{ ... }`                                                                                                                        |
| `port_expose.revoked` | An exposed port was revoked                    | `{ ... }`                                                                                                                        |

#### Pipelines

Pipeline run/step events broadcast on the **workspace** channel (not a
dedicated channel) so the Graph view can update `PipelineRunNode` status
without polling. Every payload also carries `pipeline_id`, `pipeline_slug`,
and `run_id`.

| Event Type                        | Description                              | Payload (plus the run/pipeline trio)                                                        |
| --------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------- |
| `pipeline.run.started`            | Pipeline run kicked off                  | `{ mode, author_crew_id, invoking_crew_id, invoking_agent_id, step_count, inputs_preview }` |
| `pipeline.run.completed`          | Pipeline run finished successfully       | `{ total_duration_ms, total_cost_usd }`                                                     |
| `pipeline.run.failed`             | Pipeline run failed                      | `{ failed_at_step, error_message }`                                                         |
| `pipeline.step.started`           | Step began                               | `{ step_id, step_index, step_type, tier_adapter, tier_model }`                              |
| `pipeline.step.completed`         | Step completed                           | `{ step_id, output_preview, duration_ms, cost_usd }`                                        |
| `pipeline.step.skipped`           | Step skipped (`if` condition false)      | `{ step_id, condition }`                                                                    |
| `pipeline.step.retry`             | Transient failure, step is being retried | `{ step_id, attempt, error_message_preview, sleep_ms }`                                     |
| `pipeline.step.failed`            | Step failed terminally                   | `{ step_id, error_class, error_message_preview }`                                           |
| `pipeline.step.validation_failed` | Step output failed a validation gate     | `{ step_id, reason, action }`                                                               |

#### Configuration & inbox

| Event Type                      | Description                                               | Payload                                          |
| ------------------------------- | --------------------------------------------------------- | ------------------------------------------------ |
| `inbox.updated`                 | Unified inbox changed                                     | `{ ... }`                                        |
| `instance_setting.updated`      | An instance-level setting changed                         | `{ key }`                                        |
| `feature_flag.created`          | A feature flag was created                                | `{ key }`                                        |
| `feature_flag.updated`          | A feature flag was updated                                | `{ key }`                                        |
| `feature_flag.deleted`          | A feature flag was deleted                                | `{ key }`                                        |
| `feature_flag.override_set`     | A workspace flag override was set                         | `{ ... }`                                        |
| `feature_flag.override_cleared` | A workspace flag override was cleared                     | `{ ... }`                                        |
| `workflow_template.created`     | Workflow template created                                 | `{ id }`                                         |
| `workflow_template.updated`     | Workflow template updated                                 | `{ id }`                                         |
| `workflow_template.deleted`     | Workflow template deleted                                 | `{ id }`                                         |
| `triage_rule.created`           | Triage rule created                                       | `{ id }`                                         |
| `triage_rule.updated`           | Triage rule updated                                       | `{ id }`                                         |
| `triage_rule.deleted`           | Triage rule deleted                                       | `{ id }`                                         |
| `triage.processed`              | A `POST /triage/process` batch matched at least one issue | `{ processed, matched }` (string-encoded counts) |
| `recurring_issue.created`       | Recurring issue created                                   | `{ id }`                                         |
| `recurring_issue.updated`       | Recurring issue updated                                   | `{ id }`                                         |
| `recurring_issue.deleted`       | Recurring issue deleted                                   | `{ id }`                                         |

### Crew Channel (`crew:{id}`)

| Event Type        | Description                                            | Payload                                   |
| ----------------- | ------------------------------------------------------ | ----------------------------------------- |
| `mission.created` | Mission was created in crew                            | `{ id, title }`                           |
| `file.event`      | File created/modified/deleted in the crew's output dir | `{ event, path, agent, size, timestamp }` |

<Note>
  The `files:{crewId}` channel exists for subscription (file change events) and
  shares its access rule with `crew`. File-change events are broadcast on the
  `crew:{crewId}` channel as `file.event`.
</Note>

### Mission Channel (`mission:{id}`)

| Event Type       | Description                              | Payload                 |
| ---------------- | ---------------------------------------- | ----------------------- |
| `mission.status` | Mission status changed                   | `{ id, title, status }` |
| `task.status`    | A task within the mission changed status | `{ id, status }`        |

### Session Channel (`session:{chatId}`)

#### Chat streaming events from the agent ChatHandler

| Event Type    | Description                                                              |
| ------------- | ------------------------------------------------------------------------ |
| `text`        | Text content from agent                                                  |
| `tool_call`   | Agent is calling a tool                                                  |
| `tool_result` | Tool call result                                                         |
| `thinking`    | Agent thinking/reasoning                                                 |
| `status`      | Status update                                                            |
| `done`        | Agent finished responding                                                |
| `error`       | Error occurred                                                           |
| `result`      | Final result (includes metadata: `total_cost_usd`, `num_turns`, `usage`) |
| `system`      | System message                                                           |

#### Session-scoped lifecycle events

These mirror their `*.updated`/`*.created` workspace counterparts but are
scoped to the originating chat session.

| Event Type             | Description                        | Payload                |
| ---------------------- | ---------------------------------- | ---------------------- |
| `assignment_created`   | Assignment created in this session | `{ id, target, task }` |
| `assignment_running`   | Assignment started running         | `{ ... }`              |
| `assignment_completed` | Assignment completed               | `{ ... }`              |
| `assignment_failed`    | Assignment failed                  | `{ ... }`              |
| `assignment_queued`    | Assignment queued                  | `{ ... }`              |
| `assignment_unqueued`  | Assignment unqueued                | `{ ... }`              |
| `escalation_created`   | Escalation raised in this session  | `{ id, from, reason }` |
| `escalation_resolved`  | Escalation resolved                | `{ ... }`              |
| `peer_query_running`   | Peer query started                 | `{ ... }`              |
| `peer_query_completed` | Peer query completed               | `{ ... }`              |
| `peer_query_failed`    | Peer query failed                  | `{ ... }`              |
| `port_expose_created`  | Port exposed from this session     | `{ ... }`              |

### Keeper Channel (`keeper:{workspaceId}`)

| Event Type     | Description                        | Payload                                                                                                       |
| -------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `keeper_event` | Credential access request/decision | `{ request_id, request_type, agent_name, credential_name, intent, decision, reason, risk_score, decided_at }` |

### Providers Channel (`providers`)

| Event Type        | Description                                 | Payload                                     |
| ----------------- | ------------------------------------------- | ------------------------------------------- |
| `provider_status` | Container/credential provider status update | `{ connection_id, old_status, new_status }` |

### User Channel (`user:{userId}`)

| Event Type             | Description                                 | Payload                                                |
| ---------------------- | ------------------------------------------- | ------------------------------------------------------ |
| `notification.created` | A new notification was created for the user | `{ id, action, entity_type, entity_id, entity_title }` |

<Note>
  Only the user themselves may subscribe to their own `user:{userId}` channel —
  the authorizer grants access when the connecting user's id equals the channel
  id, with no workspace membership lookup (the channel is scoped to the identity,
  not a tenant). REST (`GET /api/v1/notifications`) remains available for polling.
</Note>

***

## Connection Lifecycle

1. **Connect** -- token-authenticated WebSocket upgrade
2. **Subscribe** -- client subscribes to channels (access validated per-channel)
3. **Receive events** -- server pushes events to subscribed channels
4. **Send messages** -- client can send chat messages to session channels
5. **Keepalive** -- server sends pings every 30 seconds; client can also send pings
6. **Disconnect** -- client disconnects; all channel subscriptions are cleaned up

<Warning>
  The server maintains a send buffer of 64 messages per client. If the buffer is full, messages are dropped silently for that client — a slow consumer can miss events, so treat most payloads as refresh hints and refetch over REST.
</Warning>

***

## Example: Subscribing to Workspace Events

```javascript theme={null}
// 1. Get WS token
const response = await fetch('/api/v1/ws-token', {
  headers: { 'Authorization': 'Bearer <session-token>' }
});
const { token } = await response.json();

// 2. Connect
const ws = new WebSocket(`ws://localhost:8080/ws?token=${token}`);

ws.onopen = () => {
  // 3. Subscribe to workspace events
  ws.send(JSON.stringify({
    type: 'subscribe',
    channel: `workspace:${workspaceId}`
  }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  switch (msg.type) {
    case 'agent.log':
      console.log(`[${msg.payload.agent}] ${msg.payload.content}`);
      break;
    case 'mission.updated':
      console.log(`Mission ${msg.payload.id} -> ${msg.payload.status}`);
      break;
    case 'issue.updated':
      // Refresh issue list
      break;
  }
};
```

## Example: Interactive Chat

```javascript theme={null}
// Subscribe to session channel
ws.send(JSON.stringify({
  type: 'subscribe',
  channel: `session:${chatId}`
}));

// Send a message
ws.send(JSON.stringify({
  type: 'send_message',
  channel: `session:${chatId}`,
  payload: {
    session_id: chatId,
    content: 'Explain the authentication flow'
  }
}));

// Receive streamed response
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.channel === `session:${chatId}`) {
    switch (msg.type) {
      case 'text':
        // Append text chunk to UI
        break;
      case 'tool_call':
        // Show tool call indicator
        break;
      case 'done':
        // Agent finished
        break;
    }
  }
};

// Cancel if needed
ws.send(JSON.stringify({
  type: 'cancel_message',
  payload: { session_id: chatId }
}));
```
