# Authorizing channels

Public channels are open: any client holding the app key can subscribe. Two channel-name prefixes change that, and the prefix is the whole switch. There is nothing to configure per channel.

A channel named `private-…` requires your backend to approve each subscription. A channel named `presence-…` does the same and also attaches an identity to the subscriber, so everyone on the channel can see who else is there. Any other name is public.

Approval works this way because only your backend holds the app secret. The client asks your server to sign a specific subscription, your server decides whether that user may subscribe, and the edge verifies the signature before letting the client in. The secret never reaches the browser.

## Point the client at your endpoint

Give the client an `authEndpoint` on your own backend:

```typescript
import { BirdRealtime } from "@messagebird/realtime";

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

const room = bird.subscribe("presence-room-1");
```

The client calls that endpoint whenever it subscribes to a private or presence channel, including after a reconnect. A subscription is re-authorized against the new connection, so authorization is per connection rather than once per session.

The endpoint is same-origin by default. A cross-origin `authEndpoint` requires `allowCrossOriginAuth: true`, and any `authHeaders` you configure are sent only to a same-origin endpoint, so cookies and tokens are never handed to another origin by accident.

## What your endpoint receives and returns

The client POSTs JSON:

```json
{ "connection_id": "26896.319537", "channel_name": "presence-room-1" }
```

Respond with the signature:

```json
{ "auth": "your-app-key:8f9a…" }
```

For a presence channel, also return the member identity as a JSON **string**, the same string you signed:

```json
{
  "auth": "your-app-key:8f9a…",
  "member_data": "{\"member_id\":\"u_42\",\"member_info\":{\"name\":\"Ada\"}}"
}
```

`member_id` is the identity other members see, and the value `disconnect` acts on. `member_info` is optional free-form data that rides along to every member of the channel, so keep it small and non-sensitive.

This is where authorization actually happens. Your endpoint has the session cookie or bearer token of the caller, so it decides whether this user may join this channel, and which identity they get. Return a `403` and the client's subscription fails.

## The string you sign

Concatenate with colons, then HMAC-SHA256 with the app secret and hex-encode. Prefix the result with the app key and a colon.

| Channel type | String to sign                                 |
| ------------ | ---------------------------------------------- |
| `private-…`  | `<connection_id>:<channel_name>`               |
| `presence-…` | `<connection_id>:<channel_name>:<member_data>` |

For presence channels, `member_data` in the signed string must be byte-identical to the `member_data` you return. Sign the exact string you send, never a re-serialized copy of the same object, because key order or spacing changes the signature.

Node:

```typescript
import { createHmac } from "node:crypto";

app.post("/bird/auth", (req, res) => {
  const { connection_id, channel_name } = req.body;

  // Your own authorization decision goes here.
  const user = getUserFromSession(req);
  if (!user || !mayJoin(user, channel_name)) return res.sendStatus(403);

  const key = process.env.BIRD_REALTIME_KEY;
  const secret = process.env.BIRD_REALTIME_SECRET;

  if (channel_name.startsWith("presence-")) {
    const memberData = JSON.stringify({
      member_id: user.id,
      member_info: { name: user.name },
    });
    const sig = createHmac("sha256", secret)
      .update(`${connection_id}:${channel_name}:${memberData}`)
      .digest("hex");
    return res.json({ auth: `${key}:${sig}`, member_data: memberData });
  }

  const sig = createHmac("sha256", secret).update(`${connection_id}:${channel_name}`).digest("hex");
  res.json({ auth: `${key}:${sig}` });
});
```

Python:

```python
import hmac, hashlib, json, os

def authorize(connection_id: str, channel_name: str, user) -> dict:
    key = os.environ["BIRD_REALTIME_KEY"]
    secret = os.environ["BIRD_REALTIME_SECRET"].encode()

    if channel_name.startswith("presence-"):
        member_data = json.dumps({"member_id": user.id, "member_info": {"name": user.name}})
        to_sign = f"{connection_id}:{channel_name}:{member_data}"
        sig = hmac.new(secret, to_sign.encode(), hashlib.sha256).hexdigest()
        return {"auth": f"{key}:{sig}", "member_data": member_data}

    to_sign = f"{connection_id}:{channel_name}"
    sig = hmac.new(secret, to_sign.encode(), hashlib.sha256).hexdigest()
    return {"auth": f"{key}:{sig}"}
```

## Members and connections are not the same thing

A member is an identity. A connection is one open WebSocket. Someone with your app open in three tabs is one member with three connections, and presence events reflect the member rather than the connection. `member_added` fires when that person's first tab subscribes, and `member_removed` only when the last one leaves. Tabs in between change the channel's connection count without producing a member event.

## Common failures

A rejected subscription arrives as an error on the client. These are the four causes worth knowing:

- **Invalid signature.** The string you signed doesn't match. Almost always a re-serialized `member_data`, or a signature computed over the channel name without its `private-` or `presence-` prefix.
- **Invalid key.** The key in `auth` belongs to a different app, or it has been revoked. Rotating keys means updating both the client's `appKey` and the secret your endpoint signs with.
- **Missing member data.** A presence subscription arrived without `member_data`. Presence channels cannot be joined anonymously.
- **A `403` from your own endpoint.** Your authorization decision refused, which is the intended outcome for a user who may not join.

## Next steps

- [Send your first realtime event](/docs/get-started/send-your-first-realtime-event) is the end-to-end walkthrough this guide builds on.
- [Presence channels](/docs/guides/realtime/presence-channels) cover the member list, member events, and reading presence from your server.
- [Webhooks & events](/docs/guides/webhooks) covers `realtime.*` events, including members joining and leaving, on your own endpoint.