# Terminating member connections

Channel authorization decides who may subscribe. It does not tell the edge who a connection belongs to, so there is nothing to aim at when you need that person gone: a signed-out user keeps their socket, and keeps receiving events on channels they were already admitted to.

Signing in fixes that. A signed-in connection carries a `member_id` your backend assigned, and one API call closes every connection that member holds on the app.

Use it when a session ends outside the tab that owns it: a sign-out elsewhere, a revoked token, a ban, a password change, a seat removed from a workspace.

## 1. Sign the connection in

Point the client at an endpoint on your own backend and call `signin()` once:

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

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

const member = await bird.signin();
console.log("signed in as", member.member_id);
```

The identity belongs to the connection, not to the page, so the client signs in again automatically after a reconnect. Call `signin()` once and let it handle the rest.

Signing in is independent of subscribing. A signed-in connection still authorizes each private or presence subscription the usual way, and a connection that never signs in can still subscribe to everything it is authorized for. See [Authorizing channels](/docs/guides/realtime/authorizing-channels).

## 2. Sign the identity from your backend

The client POSTs the connection id:

```json
{ "connection_id": "26896.319537" }
```

Your endpoint answers with the identity as a JSON **string**, plus a signature over that exact string:

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

The string to sign is `<connection_id>::member::<member_data>`, with two colons on each side of `member`. HMAC-SHA256 it with the app secret, hex-encode, and prefix the app key:

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

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

  // Your own decision: who is this caller, and may they connect at all?
  const user = getUserFromSession(req);
  if (!user) return res.sendStatus(403);

  const memberData = JSON.stringify({
    member_id: user.id,
    member_info: { name: user.name },
  });
  const sig = createHmac("sha256", process.env.BIRD_REALTIME_SECRET)
    .update(`${connection_id}::member::${memberData}`)
    .digest("hex");

  res.json({ auth: `${process.env.BIRD_REALTIME_KEY}:${sig}`, member_data: memberData });
});
```

Python:

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

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

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

Sign the exact string you return. A re-serialized copy of the same object can differ in key order or spacing, and the signature is over bytes.

`member_id` is the value `disconnect` acts on, so use the identifier your own system already has for that person, as long as it fits: up to 64 characters of letters, digits, and `_ - = @ , . ;` and nothing else. That covers most user ids and plain email addresses, but not a `+` tag, a colon, or anything with a space, so map those to a safe form (a hash, or your internal numeric id) before signing. A member id outside the set is rejected at signin, which leaves the connection anonymous and nothing for `disconnect` to act on.

`member_info` is optional and, unlike on a presence channel, it stays between the client and the edge.

The string signed here is deliberately not the string a presence subscription signs, so a presence authorization can never be replayed to claim an identity, even though both payloads are called `member_data`.

## 3. Disconnect the member

```typescript
await bird.realtime.members.disconnect("rap_01krdgeqcxet5s7t44vh8rt9mg", "u_42");
```

```bash
curl -X POST \
  https://us1.platform.bird.com/v1/realtime/apps/rap_01krdgeqcxet5s7t44vh8rt9mg/members/u_42/disconnect \
  -H "Authorization: Bearer $BIRD_API_KEY" \
  -H "X-Realtime-Key: $BIRD_REALTIME_KEY" \
  -H "X-Realtime-Secret: $BIRD_REALTIME_SECRET"
```

Every connection that member holds on the app closes, wherever it is: other tabs, other devices, other networks. Connections that never signed in are unaffected, and so is a different member on the same channel.

## What the client sees

The connection closes with code `4009` and does not reconnect. A terminated connection is refused rather than dropped, so the automatic reconnection that follows a network blip does not apply here:

```typescript
bird.connection.bind("error", (e) => {
  if (e.code === 4009) showSignedOutScreen();
});
```

Reacting is your app's decision. Showing a signed-out state is usually right. Calling `bird.connect()` again works, but the new connection signs in through your endpoint, so it is worth doing only if you expect that endpoint to refuse the caller now.

A signin that fails for any other reason is reported separately, because it is not a connection failure: the socket stays up, it simply has no identity. That matters most after a reconnect, where the client re-signs in on its own and there is no promise left to reject.

```typescript
bird.connection.bind("signin_error", (e) => {
  console.warn("connection has no identity:", e.message);
});
```

While a connection is in that state it still receives events on every channel it is authorized for, but the API cannot address or disconnect it. `bird.signedInMember` is `null` whenever that is the case.

## Disconnecting is not banning

`disconnect` closes the connections that exist. It does not remember the member, and nothing stops the same browser from connecting again a second later.

Your member auth endpoint is the gate. Revoke the session (or record the ban) first, then disconnect, so the reconnect that follows is refused with a `403` instead of handed a fresh identity.

## Next steps

- [Authorizing channels](/docs/guides/realtime/authorizing-channels) covers the other signature your backend computes, for private and presence subscriptions.
- [Disconnect a member](/docs/api/reference/disconnect-realtime-app-member) is the full API reference for the request.
- [Presence channels](/docs/guides/realtime/presence-channels) explain why one member can hold several connections.