Agent index (llms.txt) — fetch this first, then the page it names.
PubSub
Real-time, WebSocket-based messaging for the Orama Network. Publish and subscribe to topics for live event streaming, notifications, inter-service communication, and collaborative features. Built on LibP2P gossipsub internally, messages propagate through the mesh of connected nodes with no single point of failure.
SDK Setup
import { createClient } from "@debros/orama";
const client = createClient({
baseURL: "https://ns-myapp.cluster.example.com",
apiKey: process.env.ORAMA_API_KEY,
});The key needs the pubsub grant (orama namespace keys create --scope invoke,pubsub --label worker); the app-runtime profile does not include it. A signed-in user's session may subscribe but not publish unless its wallet was granted pub/sub write (Access control).
All topics are scoped to your namespace automatically.
Publishing Messages
Publish string or binary payloads to any topic. Messages are base64-encoded on
the wire. Every delivered message carries both bytes, the payload exactly as
published, and data, the same payload decoded as UTF-8 text — use bytes for
anything that is not text, because the text view replaces bytes that do not form
valid UTF-8.
SDK
// Publish a string message
await client.pubsub.publish("notifications", "Hello, Network!");
// Publish binary data
const binaryData = new Uint8Array([1, 2, 3, 4]);
await client.pubsub.publish("binary-topic", binaryData);REST API
# Publish a message (data must be base64-encoded)
curl -X POST https://gateway.example.com/v1/pubsub/publish \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"topic": "notifications", "data_base64": "SGVsbG8sIE5ldHdvcmsh"}'Subscribing to Topics
Subscribe to a topic via WebSocket to receive messages in real time. Each call to subscribe opens a dedicated WebSocket connection and returns a Subscription object you can close when done.
const subscription = await client.pubsub.subscribe("notifications", {
onMessage: (msg) => {
console.log("Topic:", msg.topic);
console.log("Data:", msg.data);
console.log("Server timestamp:", new Date(msg.timestamp));
},
onError: (err) => console.error("Subscription error:", err),
onClose: (code, reason) => console.log("Subscription closed:", code, reason),
});
// Close when done
subscription.close();A subscription opened with a signed-in session is held to that session's token: the gateway closes it with 4403 within 10 seconds of the token being revoked, and with 4401 two minutes after the token expires. Subscribe again with a fresh token to continue.
Multiple Subscribers
Each subscriber gets its own WebSocket connection and its own copy of every message. You can attach as many subscribers to the same topic as you need.
const sub1 = await client.pubsub.subscribe("events", {
onMessage: (msg) => console.log("Sub1:", msg.data),
});
const sub2 = await client.pubsub.subscribe("events", {
onMessage: (msg) => console.log("Sub2:", msg.data),
});Subscription Methods
| Method | Description |
|---|---|
close() | Close the subscription and underlying WebSocket |
isConnected() | Check if the subscription is still active |
hasPresence() | Check if presence is enabled for this subscription |
getPresence() | Get current presence members (requires presence enabled) |
onMessage(handler) | Register an additional message handler. Returns unsubscribe function. |
onError(handler) | Register an additional error handler. Returns unsubscribe function. |
onClose(handler) | Register an additional close handler. Returns unsubscribe function. |
Message Format
Every message delivered to subscribers has the following shape:
interface PubSubMessage {
data: string; // The payload decoded as UTF-8 text
bytes: Uint8Array; // The payload exactly as it was published
topic: string; // The topic the message was published to
timestamp: number; // Server-assigned Unix timestamp in milliseconds
}data is lossy for a payload that is not text: bytes that do not form valid
UTF-8 are replaced with U+FFFD. Read bytes for anything binary.
Server-side timestamps are applied to all messages, so you never need to rely on client clocks for ordering.
Listing Active Topics
Retrieve all topics that currently have active subscribers in your namespace.
SDK
const topics = await client.pubsub.topics();
REST API
curl https://gateway.example.com/v1/pubsub/topics \ -H "Authorization: Bearer <token>"
Presence Tracking
Track who is online in a topic. Presence gives you real-time join/leave notifications and the ability to query current members at any time.
Subscribe with Presence
Enable presence when subscribing to a topic. Each member provides a memberId and optional meta object.
const subscription = await client.pubsub.subscribe("room.123", {
onMessage: (msg) => console.log("Message:", msg.data),
presence: {
enabled: true,
memberId: "user-alice",
meta: { displayName: "Alice", avatar: "https://..." },
onJoin: (member) => console.log(`${member.memberId} joined`),
onLeave: (member) => console.log(`${member.memberId} left`),
},
});When presence is enabled, member_id and optional member_meta are sent as WebSocket query parameters. The server broadcasts presence.join and presence.leave events to other subscribers with presence enabled.
Query Presence from a Subscription
If the subscription has presence enabled, you can query the current member list at any time:
if (subscription.hasPresence()) {
const members = await subscription.getPresence();
members.forEach((m) => console.log(`- ${m.memberId}`));
}Query Presence Without Subscribing
You do not need an active subscription to check who is present on a topic.
SDK
const presence = await client.pubsub.getPresence("room.123");
console.log(`Total members: ${presence.count}`);
presence.members.forEach((m) => console.log(`- ${m.memberId}`));REST API
curl "https://gateway.example.com/v1/pubsub/presence?topic=room.123" \ -H "Authorization: Bearer <token>"
Presence Types
interface PresenceMember {
memberId: string;
joinedAt: number;
meta?: Record<string, unknown>;
}
interface PresenceResponse {
topic: string;
members: PresenceMember[];
count: number;
}
interface PresenceOptions {
enabled: boolean;
memberId: string;
meta?: Record<string, unknown>;
onJoin?: (member: PresenceMember) => void;
onLeave?: (member: PresenceMember) => void;
}CLI
There are no dedicated CLI commands for PubSub. Use the SDK or REST API.
Patterns
Fan-Out Broadcast
Broadcast a message to all subscribers of a topic. Every subscriber receives its own copy.
await client.pubsub.publish("announcements", "Scheduled maintenance in 1 hour");Request-Reply
Implement request-reply by using a unique reply topic per request.
// Requester
const replyTopic = `reply:${crypto.randomUUID()}`;
const replySub = await client.pubsub.subscribe(replyTopic, {
onMessage: (msg) => {
console.log("Got reply:", msg.data);
replySub.close();
},
});
await client.pubsub.publish("service:lookup", JSON.stringify({
query: "user-123",
replyTo: replyTopic,
}));Live Collaboration
Combine PubSub with presence to build collaborative features like shared cursors, typing indicators, or live editing.
const collab = await client.pubsub.subscribe("doc.abc", {
onMessage: (msg) => {
const update = JSON.parse(msg.data);
applyRemoteChange(update);
},
presence: {
enabled: true,
memberId: currentUserId,
meta: { displayName: currentUser.name, color: "#3b82f6" },
onJoin: (member) => showCursor(member),
onLeave: (member) => removeCursor(member),
},
});
// Broadcast local changes
function onLocalEdit(change: object) {
client.pubsub.publish("doc.abc", JSON.stringify(change));
}REST API Reference
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/pubsub/publish | Publish a message. Body: {topic, data_base64} |
GET | /v1/pubsub/topics | List active topics in the current namespace |
GET | /v1/pubsub/presence?topic={topic} | Get presence members for a topic |
WS | /v1/pubsub/ws?topic={topic} | Subscribe to a topic via WebSocket |
WebSocket query parameters for presence:
| Parameter | Description |
|---|---|
topic | Topic to subscribe to (required) |
presence | Set to "true" to enable presence |
member_id | Your member ID (required when presence is enabled) |
member_meta | JSON-encoded metadata object (optional) |
SDK Method Reference
| Method | Signature | Description |
|---|---|---|
publish | publish(topic, data) | Publish a string or binary message to a topic |
subscribe | subscribe(topic, options?) | Subscribe to a topic via WebSocket. Returns a Subscription. |
topics | topics() | List active topics in the current namespace |
getPresence | getPresence(topic) | Get current presence members for a topic |
SubscribeOptions
interface SubscribeOptions {
onMessage?: (message: PubSubMessage) => void;
onError?: (error: Error) => void;
onClose?: (code: number, reason: string) => void;
presence?: PresenceOptions;
}Reserved topics, permissions and limits
- +Reserved topics. Everything under
_orama/belongs to the platform (today the WebRTC membership events,_orama/webrtc/<room>). Nobody publishes to it (403 PUBSUB_RESERVED_TOPIC; the platform publishes in process). Subscribing needs a pub/sub grant that covers the topic; a grantless signed-in user is refused and the topic is left out ofGET /v1/pubsub/topics. - +Reserved key. A published payload that is a JSON object with a top-level
_oramakey (any letter case) is refused with400 PUBSUB_RESERVED_KEYon every topic, and one that starts as a JSON object but is not strict JSON is refused400 PUBSUB_INVALID_OBJECT. The key marks events the platform publishes itself. Rename it in your own events. - +Who may publish. A signed-in wallet with no grant in the namespace may subscribe and read presence but not publish; the refusal is
403 INSUFFICIENT_SCOPEwithrequired_permission: pubsub:write:*. Grantpubsubwrite (aruntimerole, orpubsub:topic=chat.*), or publish from a function. Announcing presence on a subscribe socket publishes under the caller'smember_id, so it needs write too. - +An
app-runtimekey does not include pub/sub. Mint the key with--scope invoke,pubsub. - +Narrowing.
pubsub:topic=chat.*limits publish, publish-batch and the subscribe WebSocket to matching topics. - +Ordering and delivery. A message published on the same node is delivered once, not twice. The
timestampon a message is server-assigned. A dropped client is re-established with backoff (ten attempts by default). A presence member that stops answering the gateway's pings is dropped after 75 seconds. - +Trust inside the cluster. A node of the cluster that passes the overlay gate may subscribe to and publish on any
<namespace>.<topic>: the pub/sub libp2p hosts are gated to overlay addresses and to peers the gateway named, but there is no per-namespace topic authorization between nodes and no libp2p private network. Per-namespace authorization is enforced at the gateway, for your clients.