Skip to content

Messaging

There are two messaging surfaces, both riding the same shared-space transport:

  • client.message — lightweight realtime: plaintext pub/sub fan-out and end-to-end-encrypted direct messages (Double Ratchet — forward-secret, ephemeral, no history).
  • client.space — fan-out group channels with persisted history. This is what group chat is built on.

Fan out plaintext events to everyone subscribed to a subject. Good for presence, cursors, live counters, notifications.

const sub = client.message.subscribe("presence:lobby", (e) => {
console.log(e.from, e.data); // e: { subject, from, data }
});
await client.message.publish("presence:lobby", { type: "cursor", x: 12, y: 40 });
sub.unsubscribe();

send("user:<id>", payload) encrypts to the recipient with the Double Ratchet. The recipient receives DMs by subscribing to their own id:

// Recipient (ada) listens for her inbox:
client.message.subscribe("user:ada", (e) => {
console.log("DM from", e.from, ":", e.data);
});
// Sender (grace):
await client.message.send("user:ada", { text: "Hello 👋" });

Both parties must be online with the same app for the ratchet handshake to complete and the message to be delivered.

For group chat with history, use channels. A channel is a fan-out space addressed by a human name through your app’s registry. One symmetric group key seals each message once, so the server can persist + replay it — and the app’s keeper admits new members, so joining works even if no one else is online.

Create vs join is explicit: createChannel mints + registers a new channel (you become its first key-holder); joinChannel resolves an existing name and throws ChannelNotFoundError if it doesn’t exist.

// Create a channel (or join one you already made):
const space = await client.space.createChannel("project-x");
// Join an existing channel by name (admitted by the keeper):
const space = await client.space.joinChannel("project-x");
space.onMessage((e) => addMessage(e.handle, e.from, e.message.body)); // decrypted, live
await space.sendMessage("Hi everyone"); // fan-out to all
// Backfill persisted history (Double Ratchet DMs have none):
const { messages } = await space.history({ limit: 100 });
// Discover the app's channels:
const channels = await client.space.listChannels(); // [{ name, spaceId }]

You can also create a channel without a name (createSpace) and share its raw id; joinChannel accepts either a registered name or a raw space id.

Messages can be edited or deleted by their handle (the server storage key on each SpaceMessageEvent). These are author-only (enforced server-side) and mutate the persisted entry directly — an edit replaces it in place; a delete is a hard delete (gone from history).

space.onMessageEdited((e) => replace(e.handle, e.message.body)); // same handle
space.onMessageDeleted((e) => remove(e.handle));
await space.editMessage(handle, "fixed typo");
await space.deleteMessage(handle);

Whether to show “(edited)” or a deletion placeholder is an app choice — encode it in your own message body shape (e.g. { contents, edited }); the SDK and server treat the body as opaque. See the chat example.

sendEphemeral(subject, data) broadcasts a signal the server relays but never persists — the generic primitive for presence-style features (typing, online pings, cursors). Pick a subject and payload; receivers filter on it:

space.sendEphemeral("typing", { isTyping: true });
space.onEphemeral((e) => {
if (e.subject === "typing") setTyping(e.from, (e.data as { isTyping: boolean }).isTyping);
});

A space carries encrypted, erasure-coded file storage. putFile doesn’t need the socket open; reading a shared file never opens one. Ship the manifest as a normal message so members can fetch + decrypt it:

const { manifest } = await space.putFile(file, { name: file.name, type: file.type });
await space.sendMessage({ _t: "file", manifest });
// On the receiving side, detect the envelope and fetch:
const { data } = await space.getFile(manifest); // Uint8Array, decoded + decrypted

See the encrypted chat example for a full implementation and the client.space reference for signatures.