Realtime SSE Streams
Subscribe to live record writes on any agent using standard Server-Sent Events. No polling, no WebSocket handshake — open an HTTP connection and events arrive as they are written.
record.written event onto a single in-process broadcast channel. SSE handlers subscribe to that channel and push matching events to connected clients. The broadcast call is synchronous and allocation-free — zero overhead when no clients are connected.Per-agent stream
Opens a live stream scoped to one agent. Every record written to that agent is pushed as a record.written SSE event.
GET /v1/agents/orders/stream Authorization: Bearer <key>
Query parameters
| Param | Description |
|---|---|
since | HLC timestamp (ms). Backfills records written after this timestamp before going live. Capped at 1 000 most recent records. |
filter | Only emit events whose JSON payload contains this top-level field name. |
# Live stream curl -N http://localhost:7475/v1/agents/orders/stream # Backfill from a known cursor, then live curl -N "http://localhost:7475/v1/agents/orders/stream?since=1748304000000" # Only events that have a risk_score field curl -N "http://localhost:7475/v1/agents/transactions/stream?filter=risk_score"
Global stream
Subscribes to all agents on the instance. Pass an optional comma-separated agents list to narrow the stream.
GET /v1/stream Authorization: Bearer <key> # Narrow to specific agents GET /v1/stream?agents=orders,payments,inventory
Supports the same since and filter parameters as the per-agent stream.
Event format
All live-stream events use the record.written SSE event type:
event: record.written
data: {
"agent_id": "orders",
"event_type": "record.written",
"record_id": "018f3c2a-4b1d-7e8f-a3c2-1d4e5f6a7b8c",
"content_hash": "3a7bd3f1c2e9a4b5...",
"timestamp_ms": 1748304000000,
"payload": { "order_id": "ord_123", "total": 4999 }
}A heartbeat event is sent every 30 seconds to keep connections alive through proxies and load balancers:
event: heartbeat
data: { "event": "heartbeat", "subscribers": 4 }| Field | Type | Description |
|---|---|---|
agent_id | string | The agent that received the write |
event_type | string | Always "record.written" |
record_id | string | UUID of the new record |
content_hash | string | SHA-256 content hash of the record payload |
timestamp_ms | number | HLC timestamp in milliseconds |
payload | object | null | Decoded JSON payload, if the record was written as JSON |
Backfill and cursor resumption
Pass ?since=<timestamp_ms> to replay records written after a known HLC timestamp before receiving live events. SapixDB subscribes to the broadcast channel first, then replays historical records — so no event written between the two steps can be missed.
let cursor = localStorage.getItem("stream_cursor") ?? "0";
const es = new EventSource(
`/v1/agents/orders/stream?since=${cursor}`,
{ withCredentials: false }
);
es.addEventListener("record.written", (e) => {
const event = JSON.parse(e.data);
cursor = String(event.timestamp_ms);
localStorage.setItem("stream_cursor", cursor);
handle(event);
});GET /v1/agents/:id/strand/records?after=<cursor> to catch up in pages before opening the stream.Streaming query scan
Stream the results of a filtered scan as SSE events instead of waiting for the entire response to buffer. Useful for large result sets or progressive rendering.
GET /v1/agents/events/query/stream?type=scan&field=status&op=eq&value=pending&limit=500 Authorization: Bearer <key>
| Param | Description |
|---|---|
type | Query type — only scan is supported (default) |
field | Filter field name (optional) |
op | Filter operator: eq · ne · gt · lt · gte · lte · between · contains · starts_with · ends_with · like · fts · is_null · is_not_null |
value | Filter value (parsed as JSON, otherwise treated as string) |
upper | Upper bound for the between operator |
limit | Max records to stream (default 100) |
Each record is emitted as a plain data: event carrying the full RecordView JSON. A done event signals the end of the stream:
data: {"record_id":"...","content_hash":"...","payload":{...},"timestamp_ms":...}
event: done
data: {}const es = new EventSource(
"/v1/agents/events/query/stream?type=scan&field=status&op=eq&value=pending"
);
es.onmessage = (e) => {
const record = JSON.parse(e.data);
renderRow(record);
};
es.addEventListener("done", () => es.close());POST /v1/agents/:id/query instead.TypeScript SDK
The SapixDB TypeScript SDK wraps the raw EventSource API:
import { createAgentStream, createGlobalStream } from "@sapixdb/sdk";
// Subscribe to one agent
const es = createAgentStream("orders", (event) => {
console.log("new record:", event.record_id, event.payload);
});
// Backfill from a cursor, then go live
const es = createAgentStream("orders", handler, { since: lastSeenHlc });
// Watch multiple agents
const es = createGlobalStream(handler, { agents: ["orders", "payments"] });
// Filter to events that contain a specific field
const es = createGlobalStream(handler, { filter: "risk_score" });
// Always close when done
es.close();Limitations
| Limitation | Detail |
|---|---|
| Slow consumers drop events | The broadcast channel holds 1 024 events. A client that cannot keep up will miss events rather than blocking writes. Use ?since= to recover any gap on reconnect. |
| Backfill capped at 1 000 records | See the backfill section above. |
| payload is null for raw (msgpack) writes | Only JSON writes include a decoded payload. Raw binary records set payload to null. |
| No per-field filtering on the live stream | The ?filter= param matches event payloads that contain the named field — it does not support value comparisons. Use the query stream for predicate filtering. |
| Primary agent writes only for /v1/stream | The global stream and per-agent stream broadcast from the primary write path. Writes via /v1/agents/:id/records/:agent are also broadcast. |