Messaging
This content is for the 0.2.0-alpha.3 version. Switch to the latest version for up-to-date documentation.
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.
Pub/sub
Section titled “Pub/sub”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();Direct messages (end-to-end encrypted)
Section titled “Direct messages (end-to-end encrypted)”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.
Group channels (client.space)
Section titled “Group channels (client.space)”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, liveawait 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.
Editing & deleting
Section titled “Editing & deleting”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 handlespace.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.
Ephemeral signals
Section titled “Ephemeral signals”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);});Files in a channel
Section titled “Files in a channel”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 + decryptedSee the encrypted chat example for a full implementation and
the client.space reference for signatures.