Realtime security

Your session rules decide who gets to subscribe.

There is no permission model to configure in Bird. A private or presence subscription is approved by an endpoint you write, using the session you already have, and signed with a secret only your servers hold. Bird verifies the signature; you decide the policy.

auth.ts
200 · signed
// Your endpoint. The only place the app secret lives.
app.post("/bird/auth", async (req, res) => {
  const { connection_id, channel_name } = req.body;
  const user = await session(req);

  if (!mayJoin(user, channel_name)) return res.sendStatus(403);

  // Signs <connection_id>:<channel_name>[:<member_data>] with the app
  // secret, and adds shared_secret on an encrypted channel.
  res.json(
    await bird.realtime.authorizeChannel({
      connectionId: connection_id,
      channelName: channel_name,
      memberData: JSON.stringify({
        member_id: user.id,
        member_info: { name: user.name },
      }),
    }),
  );
});

One key is public. One is not.

Everything follows from that split.

Every app on the Bird Realtime API has a key and a secret. The key is meant to ship in client code; the secret authenticates your server's calls and signs subscriptions, and it is shown once, at creation. Anyone holding it can publish to your app and forge a presence identity, so treat it like a database password. Rotation is additive rather than disruptive: create a second key, deploy it, then revoke the old one.

Four controls, four questions

Who may subscribe, who may hold a connection, who can read a payload, and who is still allowed in.

  1. 01

    Signed subscriptions.

    The client posts its connection id and the channel name to your endpoint. You check the caller, refuse with a 403 if they may not join, or return an HMAC-SHA256 signature over connection id and channel name, prefixed with the app key. Because the connection id is in the signature, an approval covers one connection and cannot be replayed onto another. A presence channel signs the member identity too, and it must be the exact string you return: re-serializing the same object can reorder keys and invalidate the signature.

  2. 02

    Authorized connections.

    The app key is public, so anyone who can load your page can open a socket with it. Turn on authorized connections and every new connection has 30 seconds to prove something holding the secret vouched for it, through a private subscription or a sign-in. Anything that does not is closed with code 4009, and unauthorized connections never count against your quota. Subscribing to a public channel proves nothing and does not authorize a connection.

  3. 03

    End-to-end encryption.

    A private-encrypted- channel is sealed by your server before the request leaves your process, with a 32-byte master key that never appears in a Realtime API request. Each channel derives its own key, so authorizing a client for one encrypted channel does not let it read another. Bird cannot recover a lost key, and rotation protects future payloads rather than past ones.

  4. 04

    Revoking access now.

    A signed-out user, a changed password, a banned account: disconnect the member and every connection that identity holds closes, on every device. The clients treat that close as terminal rather than retrying, and your own endpoints stop signing for them, so they cannot come back.

Encrypted channels

Payloads Bird cannot read, on infrastructure Bird runs.

The server SDK notices the channel prefix, derives that channel's key from your master key, and seals the payload locally. The edge forwards ciphertext and your authorization endpoint hands the derived key only to clients it approves. Two limits to design around: channel and event names travel in the clear, so pick names that do not leak what you are protecting, and encryption cannot be combined with presence or with client events. Caching can be: a private-encrypted-cache- channel stores its cached event sealed.

encrypted.ts
sealed locally
const bird = new BirdClient({
  apiKey: process.env.BIRD_API_KEY,
  realtime: {
    key: process.env.BIRD_REALTIME_KEY,
    secret: process.env.BIRD_REALTIME_SECRET,
    // 32 random bytes, yours alone. Never sent to Bird.
    encryptionMasterKey: process.env.BIRD_REALTIME_MASTER_KEY,
  },
});

// Sealed in your process. The edge forwards ciphertext.
await bird.realtime.publish(APP_ID, {
  event: "order.updated",
  channels: ["private-encrypted-orders"],
  data: { order_id: "ord_123", status: "shipped" },
});

What authorization does not do.

Requiring authorized connections controls who may hold a socket open. It does not change who can read a channel: a public channel stays readable by every authorized connection, so events that belong to one customer belong on a private channel whose name your endpoint checks. And because your endpoints are the authority, a permissive endpoint hands out access as freely as a leaked key would. The same honesty applies to client events, which the edge does not validate: use them for signals, and route anything authoritative through your server.

Where the data sits.

An app picks its region when you create it and keeps it for life, so you choose the one closest to your users, and an app's region can differ from your workspace's home region. Apps are also the isolation boundary: two apps never see each other's channels, which is what makes one app per environment the right way to keep staging traffic out of production.

Go deeper in the docs.

Authorizing channels has the request and response contract and the exact string to sign. Requiring authorized connections covers the 30-second window and code 4009, encrypted channels covers key generation and rotation, and terminating member connections is the sign-in and disconnect flow.

Ship the key. Keep the secret.

Signed subscriptions, authorized connections, and encrypted channels are part of every Realtime app, on every plan.

从一个渠道开始。
准备好后,再添加其他渠道。

测试 API 密钥即刻可用。添加支付方式并验证发送者身份后,即可解锁生产环境。

正在使用 Claude Code、Cursor 或 Codex?复制一条设置提示,您的智能代理即可自动安装 Bird CLI 和相关技能。选择您的工具:

Cursor