Give a channel a symmetric key and every payload is encrypted with AES-GCM before it leaves your process. The service routes and stores only ciphertext, and clients holding the same key decrypt transparently. The key is shared between your clients out of band and is never sent to Foony.
Encrypting a channel
Pass a cipher when you get the channel. Publishing and subscribing then work exactly as they do on a plain channel. For example:
import { Realtime, generateRandomKey } from '@foony/realtime';
const key = await generateRandomKey(); // 256-bit key as a base64 string
const channel = realtime.channels.get('secure:room', { cipher: { key } });
channel.subscribe((message) => {
console.log(message.data); // Already decrypted
});
await channel.publish('chat', { text: 'only we can read this' });
Encryption runs on WebCrypto, so the same code works in browsers and in Node 20 or newer.
Keys
A key is 16 or 32 raw bytes, passed either as a Uint8Array or as a base64 string. The length picks the strength. A 32-byte key encrypts with AES-256 and a 16-byte key with AES-128. generateRandomKey() returns a base64 256-bit key by default, and generateRandomKey(128) the shorter kind.
Generating, storing, and distributing the key is your application’s job. Treat it like any other secret. Share it only with the clients that should read the channel, and never send it to Foony.
What is and isn’t encrypted
Only the payload is encrypted. The service still needs the rest of the message to route it:
| Encrypted | Plaintext |
|---|---|
Message data |
Message name |
Presence payloads from enter and update |
Channel name, clientId, timestamps |
Anyone allowed to subscribe can still see that messages are flowing, who publishes them, and the message names. If those are sensitive too, put them inside the payload.
Reading without the key
A subscriber that got the channel without a cipher still receives every message. The data arrives as an opaque base64 string and the message’s encoding field names the cipher, so you can tell the payload is encrypted rather than corrupt.
A subscriber with the wrong key can’t be given the payload at all. The SDK drops the message and logs a warning, since AES-GCM authenticates the ciphertext and a failed decrypt means the wrong key or a tampered message.
History and REST
Stored messages are ciphertext too. Pass the same cipher to read them back decrypted, from the realtime channel’s history() or over REST. For example:
const rest = new Rest({ key: process.env.FOONY_API_KEY! });
const channel = rest.channels.get('secure:room', { cipher: { key } });
const page = await channel.history({ limit: 100 });
A stored message that the current key can’t decrypt, for example one encrypted before a key change, comes back with its data still encrypted instead of being dropped.
Changing the key
A channel keeps the cipher it was created with, because channels.get returns the same instance per name and only the first call’s options apply. To move to a new key, release the channel and get it again, or start a new channel name:
realtime.channels.release('secure:room');
const channel = realtime.channels.get('secure:room', { cipher: { key: newKey } });
Messages encrypted under the old key stay unreadable under the new one, so clients that need old history must keep the old key around to read it.