---
title: Socket Transport
description: Using WebSocket or SockJS for real-time streaming with Octavus.
---

# Socket Transport

The socket transport enables real-time bidirectional communication using WebSocket or SockJS. Use this when you need persistent connections, custom server events, or when HTTP/SSE isn't suitable for your infrastructure.

## When to Use Socket Transport

| Use Case | Recommended Transport |
|----------|----------------------|
| Standard web apps (Next.js, etc.) | HTTP (`createHttpTransport`) |
| Real-time apps with custom events | Socket (`createSocketTransport`) |
| Apps behind proxies that don't support SSE | Socket |
| Need for typing indicators, presence, etc. | Socket |
| Meteor, Phoenix, or socket-based frameworks | Socket |

## Patterns Overview

There are two main patterns for socket-based integrations:

| Pattern | When to Use |
|---------|-------------|
| [Server-Managed Sessions](#server-managed-sessions-recommended) | **Recommended.** Server creates sessions lazily. Client doesn't need sessionId. |
| [Client-Provided Session ID](#client-provided-session-id) | When client must control session creation or pass sessionId from URL. |

## Server-Managed Sessions (Recommended)

The cleanest pattern is to have the server manage session lifecycle. The client never needs to know about `sessionId` — the server creates it lazily on first message.

### Client Setup

```typescript
import { useMemo } from 'react';
import SockJS from 'sockjs-client';
import { useOctavusChat, createSocketTransport, type SocketLike } from '@octavus/react';

function connectSocket(): Promise<SocketLike> {
  return new Promise((resolve, reject) => {
    const sock = new SockJS('/octavus');
    sock.onopen = () => resolve(sock);
    sock.onerror = () => reject(new Error('Connection failed'));
  });
}

function Chat() {
  // Transport is stable — no dependencies on sessionId
  const transport = useMemo(
    () => createSocketTransport({ connect: connectSocket }),
    [],
  );

  const { messages, status, send } = useOctavusChat({ transport });

  const sendMessage = async (text: string) => {
    await send(
      'user-message',
      { USER_MESSAGE: text },
      { userMessage: { content: text } },
    );
  };

  // ... render messages
}
```

### Server Setup (Express + SockJS)

The server creates a session on first trigger message:

```typescript
import sockjs from 'sockjs';
import { OctavusClient, type AgentSession } from '@octavus/server-sdk';

const client = new OctavusClient({
  baseUrl: process.env.OCTAVUS_API_URL!,
  apiKey: process.env.OCTAVUS_API_KEY!,
});

function createSocketHandler() {
  return (conn: sockjs.Connection) => {
    let session: AgentSession | null = null;
    let abortController: AbortController | null = null;

    conn.on('data', (rawData: string) => {
      void handleMessage(rawData);
    });

    async function handleMessage(rawData: string) {
      const msg = JSON.parse(rawData);

      if (msg.type === 'stop') {
        abortController?.abort();
        return;
      }

      if (msg.type === 'trigger') {
        // Create session lazily on first trigger
        if (!session) {
          const sessionId = await client.agentSessions.create('your-agent-id', {
            // Initial input variables
            COMPANY_NAME: 'Acme Corp',
          });
          session = client.agentSessions.attach(sessionId, {
            tools: {
              // Your tool handlers
            },
          });
        }

        abortController = new AbortController();

        // Iterate events directly — no SSE parsing needed
        const events = session.trigger(msg.triggerName, msg.input);

        try {
          for await (const event of events) {
            if (abortController.signal.aborted) break;
            conn.write(JSON.stringify(event));
          }
        } catch {
          // Handle errors
        }
      }
    }

    conn.on('close', () => abortController?.abort());
  };
}

const sockServer = sockjs.createServer({ prefix: '/octavus' });
sockServer.on('connection', createSocketHandler());
sockServer.installHandlers(httpServer);
```

**Benefits of this pattern:**
- Client code is simple — no sessionId management
- No transport caching issues
- Session is created only when needed
- Server controls session configuration

## Client-Provided Session ID

If you need the client to control the session (e.g., resuming a specific session from URL), pass the sessionId in an init message after connecting:

```typescript
import { useMemo } from 'react';
import SockJS from 'sockjs-client';
import { useOctavusChat, createSocketTransport, type SocketLike } from '@octavus/react';

function Chat({ sessionId }: { sessionId: string }) {
  const transport = useMemo(
    () =>
      createSocketTransport({
        connect: () =>
          new Promise((resolve, reject) => {
            const sock = new SockJS('/octavus');
            sock.onopen = () => {
              // Send init message with sessionId
              sock.send(JSON.stringify({ type: 'init', sessionId }));
              resolve(sock);
            };
            sock.onerror = () => reject(new Error('Connection failed'));
          }),
      }),
    [sessionId],
  );

  const { messages, status, send } = useOctavusChat({ transport });
  // ... render chat
}
```

When `sessionId` changes, the hook automatically reinitializes with the new transport.

### Server Handler with Init Message

When using client-provided sessionId, the server must handle an `init` message:

```typescript
sockServer.on('connection', (conn) => {
  let session: AgentSession | null = null;
  let abortController: AbortController | null = null;

  conn.on('data', (rawData: string) => {
    void handleMessage(rawData);
  });

  async function handleMessage(rawData: string) {
    const msg = JSON.parse(rawData);

    // Handle session initialization
    if (msg.type === 'init') {
      session = client.agentSessions.attach(msg.sessionId, {
        tools: { /* ... */ },
      });
      return;
    }

    if (msg.type === 'stop') {
      abortController?.abort();
      return;
    }

    // All other messages require initialized session
    if (!session) {
      conn.write(JSON.stringify({
        type: 'error',
        errorText: 'Session not initialized. Send init message first.',
      }));
      return;
    }

    if (msg.type === 'trigger') {
      // ... handle trigger (same as server-managed pattern)
    }
  }
});
```

## Async Session ID

When the session ID is fetched asynchronously (e.g., from an API), you have two options:

### Option 1: Conditionally Render (Recommended)

Don't render the chat component until `sessionId` is available:

```tsx
function ChatPage() {
  const [sessionId, setSessionId] = useState<string | null>(null);

  useEffect(() => {
    api.createSession().then(res => setSessionId(res.sessionId));
  }, []);

  // Don't render until sessionId is ready
  if (!sessionId) {
    return <LoadingSpinner />;
  }

  return <Chat sessionId={sessionId} />;
}
```

This is the cleanest approach — the `Chat` component always receives a valid `sessionId`.

### Option 2: Server-Managed Sessions

Use the [server-managed sessions pattern](#server-managed-sessions-recommended) where the server creates the session lazily. The client never needs to know about `sessionId`.

## Native WebSocket

If you're using native WebSocket instead of SockJS, you can pass sessionId via URL:

```typescript
const transport = useMemo(
  () =>
    createSocketTransport({
      connect: () =>
        new Promise((resolve, reject) => {
          const ws = new WebSocket(
            `wss://your-server.com/octavus?sessionId=${sessionId}`
          );
          ws.onopen = () => resolve(ws);
          ws.onerror = () => reject(new Error('WebSocket connection failed'));
        }),
    }),
  [sessionId],
);
```

When `sessionId` changes, the hook automatically reinitializes with the new transport.

## Custom Events

Handle custom events alongside Octavus stream events:

```typescript
const transport = createSocketTransport({
  connect: connectSocket,

  onMessage: (data) => {
    const msg = data as { type: string; [key: string]: unknown };

    switch (msg.type) {
      case 'typing-indicator':
        setAgentTyping(msg.isTyping as boolean);
        break;

      case 'presence-update':
        setOnlineUsers(msg.users as string[]);
        break;

      case 'notification':
        showToast(msg.message as string);
        break;

      // Octavus events (text-delta, finish, etc.) are handled automatically
    }
  },
});
```

## Connection Management

### Handling Disconnections

```typescript
const transport = createSocketTransport({
  connect: connectSocket,

  onClose: () => {
    console.log('Socket disconnected');
    setConnectionStatus('disconnected');
  },
});
```

### Reconnection with Exponential Backoff

```typescript
import { useRef, useCallback, useMemo } from 'react';
import { createSocketTransport, type SocketLike } from '@octavus/react';

function useReconnectingTransport() {
  const reconnectAttempts = useRef(0);
  const maxAttempts = 5;

  const connect = useCallback((): Promise<SocketLike> => {
    return new Promise((resolve, reject) => {
      const sock = new SockJS('/octavus');

      sock.onopen = () => {
        reconnectAttempts.current = 0;
        resolve(sock);
      };

      sock.onerror = () => {
        if (reconnectAttempts.current < maxAttempts) {
          reconnectAttempts.current++;
          const delay = Math.min(1000 * 2 ** reconnectAttempts.current, 30000);
          console.log(`Reconnecting in ${delay}ms...`);
          setTimeout(() => connect().then(resolve).catch(reject), delay);
        } else {
          reject(new Error('Max reconnection attempts reached'));
        }
      };
    });
  }, []);

  return useMemo(
    () => createSocketTransport({ connect }),
    [connect],
  );
}
```

## Framework Notes

### Meteor

Meteor's bundler may have issues with ES6 imports of `sockjs-client`. Use `require()` instead:

```typescript
// ❌ May fail in Meteor
import SockJS from 'sockjs-client';

// ✅ Works in Meteor
const SockJS: typeof import('sockjs-client') = require('sockjs-client');
```

### SockJS vs WebSocket

| Feature | WebSocket | SockJS |
|---------|-----------|--------|
| Browser support | Modern browsers | All browsers (with fallbacks) |
| Session ID | Via URL query params | Via init message |
| Proxy compatibility | Varies | Excellent (polling fallback) |
| Setup complexity | Lower | Higher (requires server library) |

## Protocol Reference

### Client → Server Messages

```typescript
// Initialize session (only for client-provided sessionId pattern)
{ type: 'init', sessionId: string }

// Trigger an action
{ type: 'trigger', triggerName: string, input?: Record<string, unknown> }

// Stop current stream
{ type: 'stop' }
```

### Server → Client Messages

The server sends Octavus `StreamEvent` objects as JSON. See [Streaming Events](/docs/server-sdk/streaming#event-types) for the full list.

```typescript
// Examples
{ type: 'start', messageId: '...' }
{ type: 'text-delta', id: '...', delta: 'Hello' }
{ type: 'tool-input-start', toolCallId: '...', toolName: 'get-user' }
{ type: 'finish', finishReason: 'stop' }
{ type: 'error', errorText: 'Something went wrong' }
```

## Full Example

For a complete walkthrough of building a chat interface with SockJS, see the [Socket Chat Example](/docs/examples/socket-chat).
