Documentation
Sign inGet started

Encrypted channels

A channel whose name starts with private-encrypted- is end-to-end encrypted. Your server seals each payload before publishing it, and approved browser clients decrypt it with a key from your authorization endpoint. The Realtime edge and network intermediaries see only ciphertext.
Generate and store a 32-byte master key. The master key never appears in a Realtime API request, and the channel-name prefix enables the feature. Bird cannot recover a lost key, and payloads sealed with that key remain unreadable after you replace it.
Encrypted channels use the same endpoint and signature as private channels. The authorization response also includes the channel's derived decryption key as shared_secret. Rejecting a subscription prevents that client from receiving the key.

Generate a master key

Generate 32 random bytes, encode them as base64, and store the value like the app secret:
Codebeispiel
openssl rand -base64 32
Give it to your server SDK as part of the realtime configuration, next to the app key and secret.

Publish an encrypted event

The server SDK detects the channel prefix, derives its key from the master key, and seals the JSON payload locally. The publish request contains the sealed envelope.
import { BirdClient } from "@messagebird/sdk";

const bird = new BirdClient({
  apiKey: process.env.BIRD_API_KEY,
  realtime: {
    key: process.env.BIRD_REALTIME_KEY,
    secret: process.env.BIRD_REALTIME_SECRET,
    encryptionMasterKey: process.env.BIRD_REALTIME_MASTER_KEY,
  },
});

await bird.realtime.publish("rap_01krdgeqcxet5s7t44vh8rt9mg", {
  event: "order.updated",
  channels: ["private-encrypted-orders"],
  data: { order_id: "ord_123", status: "shipped" },
});
An encrypted channel must be the only channel in a single publish. Each encrypted channel derives a different key, so other channels could not decrypt the same sealed payload. The SDKs reject this fan-out locally, and the API returns E23000 if it receives one. To publish to several encrypted channels, use a batch with one channel per event.

Return the shared secret from your auth endpoint

Your auth endpoint approves encrypted subscriptions the way it approves private ones. Use the SDK's authorizeChannel helper and the response gains the shared_secret automatically whenever the channel name carries the encrypted prefix:
app.post("/bird/auth", async (req, res) => {
  const { connection_id, channel_name } = req.body;

  const user = getUserFromSession(req);
  if (!user || !mayJoin(user, channel_name)) return res.sendStatus(403);

  res.json(
    await bird.realtime.authorizeChannel({
      connectionId: connection_id,
      channelName: channel_name,
    }),
  );
});
The SDK derives a separate shared_secret for each channel. Authorization for private-encrypted-orders therefore does not decrypt private-encrypted-invoices. The secret travels in your authorization response and is not included in the subscription frame sent to the edge.

Subscribe and decrypt in the browser

The cipher uses the separate @messagebird/realtime/encrypted entry point. Import it and pass it as the client's encryption option:
Codebeispiel
import { BirdRealtime } from "@messagebird/realtime";
import { encryption } from "@messagebird/realtime/encrypted";

const bird = new BirdRealtime({
  appKey: "your-app-key",
  region: "us1",
  authEndpoint: "/bird/auth",
  encryption,
});

const orders = bird.subscribe("private-encrypted-orders");
orders.bind("order.updated", (data) => {
  console.log(data); // decrypted: { order_id: "ord_123", status: "shipped" }
});
Bindings receive plaintext. Subscribing without the encryption option throws immediately, and an authorization response without shared_secret fails the subscription.
Only the browser client currently receives encrypted channels. The Swift and Kotlin clients reject private-encrypted- subscriptions because they do not implement decryption.

Rotate the master key

Deploy the new key to every publisher and authorization endpoint together. During rotation:
  1. New publishes seal under the new key.
  2. A subscribed browser client that cannot decrypt an event re-authorizes once and retrieves the new shared_secret.
  3. Instances using different master keys can briefly publish events that some clients cannot decrypt, so coordinate the rollout across instances.
Rotate a leaked or lost key. Rotation protects future payloads but cannot re-seal earlier events or revoke copies of the old key.

What encrypted channels do not do

  • Official clients do not support client events. Browser trigger() throws on encrypted channels because the client does not seal client-to-client payloads. Do not send plaintext client events from a custom client.
  • Presence and encryption cannot be combined. The presence-encrypted- prefix is unsupported. Cache and encryption work together: private-encrypted-cache- channels store the cached event sealed, though after a key rotation the cached copy stays sealed under the old key until the next publish replaces it.
  • Channel names and event names are not encrypted. Only the payload is. Pick channel names that do not leak what you are protecting.
  • The Realtime edge cannot inspect payloads. Channel and event names remain visible, while the payload stays encrypted.

Next steps