SapixDBSapixDB/Docs
Home
Add-on

Chat Add-on

Real-time multi-room messaging backed by the immutable strand. Agents ask questions, humans answer — every message is a cryptographically signed record with the same integrity guarantees as your data.

💬 One env var to activate: SAPIX_CHAT_ENABLED=true
No extra dependencies. SSE streaming. Messages are strand records — permanent, signed, auditable.

Enable

Environment variables
SAPIX_CHAT_ENABLED=true

Rooms

Rooms are persistent named channels. Create a room once — it persists across agent restarts. Room metadata (name, description, members) is stored in the GraphIndex meta column family.

TypeScript — create and manage rooms
// Create a room
const r = await fetch("http://localhost:7475/v1/chat/rooms", {
  method: "POST",
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
  body: JSON.stringify({
    name: "dba-alerts",
    description: "DBA Agent operational alerts and approvals",
    creator_id: "dba-agent",
  }),
});
const { room_id } = await r.json();

// List all rooms
const rooms = await fetch("http://localhost:7475/v1/chat/rooms", {
  headers: { Authorization: `Bearer ${apiKey}` },
});

// Add a member
await fetch(`http://localhost:7475/v1/chat/rooms/${room_id}/members`, {
  method: "POST",
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
  body: JSON.stringify({ user_id: "usr_operator_123" }),
});

Messages

Messages are written directly to the strand — the same append-only, cryptographically linked record store used for all agent data. This means every message is permanently retained, tamper-evident, and exportable for compliance purposes.

TypeScript — send and retrieve messages
// Send a message from an agent
const msg = await fetch(`http://localhost:7475/v1/chat/rooms/${room_id}/messages`, {
  method: "POST",
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
  body: JSON.stringify({
    sender_id: "dba-agent",
    body: "Table bloat on `users` is at 78%. Should I schedule a VACUUM now? Reply YES or NO.",
  }),
});
const message = await msg.json();
// { message_id, room_id, sender_id, body, created_at_secs }

// Retrieve recent messages (newest first)
const history = await fetch(
  `http://localhost:7475/v1/chat/rooms/${room_id}/messages?limit=50`,
  { headers: { Authorization: `Bearer ${apiKey}` } },
);
const { messages } = await history.json();

SSE Stream — Real-Time Updates

Subscribe to a room's stream using Server-Sent Events. The stream delivers new messages within milliseconds of a write. Keep-alive pings are sent every 15 seconds to prevent connection timeout.

TypeScript — browser SSE
// Browser — EventSource
const es = new EventSource(`http://localhost:7475/v1/chat/rooms/${room_id}/stream`);
// Note: EventSource doesn't support custom headers in some browsers.
// Use a server-side proxy or pass apiKey as a query parameter for authenticated streams.

es.onmessage = (evt) => {
  const { room_id, message } = JSON.parse(evt.data);
  console.log(`[${message.sender_id}] ${message.body}`);
  // Update your UI here
};

es.onerror = () => es.close(); // Reconnect logic optional
TypeScript — Node.js / server-side
import { EventSource } from "eventsource"; // npm install eventsource

const es = new EventSource(
  `http://localhost:7475/v1/chat/rooms/${room_id}/stream`,
  { headers: { Authorization: `Bearer ${apiKey}` } },
);

es.onmessage = async (evt) => {
  const { message } = JSON.parse(evt.data);
  // Process human response to agent question
  if (message.body.toUpperCase() === "YES") {
    await scheduleVacuum();
  }
};

SSE event format

// Each SSE event data field contains:
{
  "room_id": "room_a3f9...",
  "message": {
    "message_id": "msg_b2c1...",
    "room_id":    "room_a3f9...",
    "sender_id":  "dba-agent",
    "body":       "Table bloat at 78%. Proceed?",
    "created_at_secs": 1749427260
  }
}

Agent Integration Pattern

The canonical pattern for human-in-the-loop agent workflows: the agent sends a message, opens a stream subscription, and waits for a response before proceeding.

TypeScript — agent asking for human approval
import { EventSource } from "eventsource";

async function requestApproval(
  roomId: string,
  question: string,
  timeoutMs = 300_000, // 5 minute timeout
): Promise<boolean> {
  // 1. Open the stream first (so we don't miss the response)
  return new Promise((resolve) => {
    const es = new EventSource(
      `http://localhost:7475/v1/chat/rooms/${roomId}/stream`,
      { headers: { Authorization: `Bearer ${apiKey}` } },
    );

    const timer = setTimeout(() => {
      es.close();
      resolve(false); // Timeout — default deny
    }, timeoutMs);

    es.onmessage = (evt) => {
      const { message } = JSON.parse(evt.data);
      const answer = message.body.trim().toUpperCase();
      if (answer === "YES" || answer === "APPROVE") {
        clearTimeout(timer);
        es.close();
        resolve(true);
      } else if (answer === "NO" || answer === "DENY") {
        clearTimeout(timer);
        es.close();
        resolve(false);
      }
    };

    // 2. After opening stream, send the question
    fetch(`http://localhost:7475/v1/chat/rooms/${roomId}/messages`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
      body: JSON.stringify({ sender_id: "dba-agent", body: question }),
    });
  });
}

// Usage
const approved = await requestApproval(
  "dba-alerts",
  "Index on users.email will reduce query time by 85%. Apply? Reply YES or NO.",
);
if (approved) await applyIndex();

HTTP API Reference

MethodPathDescription
POST/v1/chat/roomsCreate a room
GET/v1/chat/roomsList all rooms
GET/v1/chat/rooms/:idGet a room
DELETE/v1/chat/rooms/:idDelete a room (strand tombstone written)
POST/v1/chat/rooms/:id/membersAdd a member
DELETE/v1/chat/rooms/:id/members/:user_idRemove a member
POST/v1/chat/rooms/:id/messagesSend a message (written to strand + broadcast)
GET/v1/chat/rooms/:id/messages?limit=50Get recent messages
GET/v1/chat/rooms/:id/streamSSE stream — real-time messages for this room

Strand audit records

Payload typeTrigger
chat/messageEvery send_message() call
chat/room_deletedRoom deletion

Configuration

Environment variableDefaultDescription
SAPIX_CHAT_ENABLEDfalseEnable the chat add-on

Known Limitations

  • Message retrieval is O(n) in strand size. GET /messages scans the strand for records matching the room. For high-write agents, run a dedicated chat agent with a separate data directory.
  • Broadcast capacity is 1024. Slow SSE clients that fall more than 1024 messages behind will miss events. Use GET /messages to catch up.
  • No per-room access control. Any API key can read any room. Per-room ACL is planned via the Policy Engine.
  • Messages are immutable. The strand is append-only — messages cannot be edited or deleted. A tombstone pattern for individual messages is planned.
  • SSE auth in browsers. The browser EventSource API does not support custom headers in all implementations. Use a server-side proxy or query-parameter token for authenticated browser streams.
→ Auth Add-on→ Mail Add-on→ Realtime SSE