Docs/Core concepts/Encryption

Encryption

Private-encrypted channels give authorized subscribers a per-channel key and decrypt payloads locally in the browser.

Concept· 5 min read

Encrypted channels

A private-encrypted- channel is a private channel whose payloads are encrypted end to end. The gateway and WebSocket layer only ever see ciphertext; the browser decrypts locally. The live debug console shows such publishes as their encrypted wire envelope — it does not decrypt them.

How keys are delivered

When an encrypted channel is authorized for a user, the channel-auth response supplies a per-channel shared key alongside the subscription token. The SDK keeps that key in memory and uses it to decrypt incoming envelopes. Authorization follows the same private-channel callback as any other private channel.

The decryption key is scoped to a single channel and is delivered only to subscribers your callback authorizes. It is never embedded in client code.

Subscribing

Subscribing is identical to any private channel — the SDK handles decryption transparently and emits decryption_error if an envelope cannot be decrypted.

encrypted.ts
const channel = await emitwave.subscribe('private-encrypted-account.42')

channel.bind('message.created', (data) => {
  // data has already been decrypted locally by the SDK
  console.log(data)
})

channel.bind('decryption_error', () => {
  // The encrypted payload was invalid or could not be decrypted
})

Publishing encrypted data

The current server SDKs publish the JSON you give them unchanged; they do not build the ciphertext envelope. To publish to an encrypted channel, create a compatible ciphertext envelope on a trusted server with a compatible encryption publisher, then publish the outer event as encrypted.

Never publish plaintext under an encrypted channel name. A plaintext payload will not decrypt and subscribers will see decryption_error.

Decryption errors

The reserved decryption_error event fires when an encrypted payload is invalid or cannot be decrypted. Like cache_miss, it is not delivered to bindAll(). Bind it explicitly to surface a degraded state in your UI.