Skip to main content

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.

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). The handshake skips origin checks — authentication is token-based only.

Message Format

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

Client Messages

{
  type: "subscribe" | "unsubscribe" | "ping" | "send_message" | "cancel_message",
  channel?: string,
  payload?: any
}

Server Messages

{
  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.
{
  "type": "subscribe",
  "channel": "workspace:ws_abc123"
}
If access is denied, the server responds with:
{
  "type": "error",
  "channel": "workspace:ws_abc123",
  "payload": { "error": "access denied" }
}

unsubscribe

{
  "type": "unsubscribe",
  "channel": "workspace:ws_abc123"
}

ping

Keep-alive ping. The server also sends periodic pings every 30 seconds.
{
  "type": "ping"
}
Server responds with:
{
  "type": "pong",
  "payload": null
}

send_message

Send a chat message to an agent session.
{
  "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.
{
  "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

ChannelFormatAccess RuleDescription
workspaceworkspace:{workspaceId}Workspace memberWorkspace-wide events
crewcrew:{crewId}Member of crew’s workspaceCrew-specific events
agentagent:{agentId}Member of agent’s workspaceAgent-specific events
sessionsession:{chatId}Member of chat’s workspaceInteractive chat session
missionmission:{missionId}Member of mission’s workspaceMission status updates
keeperkeeper:{workspaceId}Workspace memberKeeper credential access events
filesfiles:{crewId}Member of crew’s workspaceFile change events
providersprovidersAny authenticated userProvider status updates

Server Event Types

Events are broadcast to channels when state changes occur. Below are the event types by channel.

Workspace Channel (workspace:{id})

Event TypeDescriptionPayload
crew.createdCrew was created{ id, name, slug }
crew.updatedCrew was updated{ id }
crew.deletedCrew was deleted{ id }
agent.logAgent log output{ ts, level, agent, agent_id, event, content, metadata }
mission.updatedMission status changed{ id, crew_id, status }
issue.createdIssue was created{ id }
issue.updatedIssue was updated{ id }
issue.deletedIssue was deleted{ identifier }
issue.startedIssue execution started{ id, identifier, status }
project.createdProject was created{ id }
project.updatedProject was updated{ id }
project.deletedProject was deleted{ id }
integration.updatedIntegration was changed{ id }

Crew Channel (crew:{id})

Event TypeDescriptionPayload
mission.createdMission was created in crew{ id, title }

Mission Channel (mission:{id})

Event TypeDescriptionPayload
mission.statusMission status changed{ id, status }
mission.updatedMission was updated{ id, status }

Session Channel (session:{chatId})

Chat streaming events from the agent ChatHandler:
Event TypeDescription
textText content from agent
tool_callAgent is calling a tool
tool_resultTool call result
thinkingAgent thinking/reasoning
statusStatus update
doneAgent finished responding
errorError occurred
resultFinal result (includes metadata: total_cost_usd, num_turns, usage)
systemSystem message

Keeper Channel (keeper:{workspaceId})

Event TypeDescriptionPayload
keeper_eventCredential access request/decision{ request_id, request_type, agent_name, credential_name, intent, decision, reason, risk_score, decided_at }

Providers Channel (providers)

Event TypeDescriptionPayload
provider_statusContainer/credential provider status update{ provider, status, details }

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
The server maintains a send buffer of 64 messages per client. If the buffer is full, messages are dropped silently for that client.

Example: Subscribing to Workspace Events

// 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

// 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 }
}));