Topics & Messages¶
Topics are named, ordered channels within a space. Every message belongs to exactly one topic. You can have as many topics as you like — "general", "alerts", "notifications" — and each maintains its own independent message chain.
Topic IDs must be 2–64 characters of lowercase letters, digits, hyphens, or underscores, starting and ending with an alphanumeric character (e.g. general, user-notes, audit_log).
The message chain¶
Messages in a topic form a hash chain: each message records the hash of the previous one. This gives you a tamper-evident, append-only log. If any message is altered after the fact, every subsequent hash would be wrong, and the SDK detects it.
sequenceDiagram
participant C as Client
participant S as Server
C->>S: POST /spaces/{id}/topics/{topic}/messages<br/>{type, data, prev_hash, sender, signature}
S-->>C: {message_hash, server_timestamp}
C->>S: GET /spaces/{id}/topics/{topic}/messages
S-->>C: [{message_hash, prev_hash, data, ...}, ...]
C->>C: Validate chain & decrypt
Each message on the wire looks like:
| Field | Description |
|---|---|
message_hash |
M-prefixed hash of this message's content |
prev_hash |
Hash of the previous message (or null for the first) |
type |
Application-defined string, e.g. "chat.text", "file.upload" |
data |
Base64-encoded AES-GCM ciphertext |
sender |
User ID of the author |
signature |
Ed25519 signature over the message content |
server_timestamp |
When the server received the message (milliseconds UTC) |
Encryption¶
All message data is encrypted before it leaves the client. The server stores only ciphertext.
Key derivation follows this tree:
symmetricRoot
└── message_key (HKDF "message key | {spaceId}")
└── topic_key (HKDF "topic key | {topicId}")
Each topic has its own derived key, so one topic's messages cannot be decrypted with another topic's key.
Sending a message¶
Reading messages¶
Message ordering¶
Messages are returned in the order the server received them (by server_timestamp). The SDK's chain validation independently verifies that the prev_hash links are correct — so you always get a consistent, ordered, tamper-evident log regardless of whether you trust the server's timestamp.
You can query by time range:
Message types¶
The type field is a free-form string you define for your application. A simple convention is "category.action":
| Type | Meaning |
|---|---|
chat.text |
Plain text chat message |
chat.image |
Image attachment (data holds a blob ID) |
file.upload |
File was uploaded |
event.join |
User joined |
The server doesn't interpret or filter by type — that's entirely up to your application.
Real-time updates (WebSocket)¶
You can subscribe to a topic's live stream over WebSocket:
Related concepts¶
- Spaces — the container that holds topics
- Access Control — who can post to a topic
- Blobs — for large file payloads referenced from messages