---
sidebar_position: 2
---

# Channels

A channel is a name browsers listen to. Your server publishes to it, and browsers on private and presence channels can send each other events too.

## Names and kinds

Up to 200 characters of `A-Z a-z 0-9 _ - = @ , . ;`. The start of the name says its kind, as Pusher's do:

| Kind | Starts with | Who may listen | Sealed |
|---|---|---|---|
| Public | anything else | anyone with your app's key | no: anything there is public |
| Private | `private-` | whoever your server lets in | yes, with the channel's key |
| Presence | `presence-` | whoever your server lets in, listed to everyone on it | yes, with the channel's key |

A channel lives within your app: another app's `orders` is another channel. `private-encrypted-…` isn't taken.

## Owning a channel

Your server owns names by their start, and answers for each user who asks:

**Node**

```ts
realtime.channel( 'private-orders-', async ( user, channel ) => {
    const order = await findOrder( channel.slice( 'private-orders-'.length ) );
    return order !== null && order.userId === user;
} );
```

**PHP**

```php
$realtime->channel( 'private-orders-', function ( $user, $channel ) {
    $order = find_order( substr( $channel, strlen( 'private-orders-' ) ) );
    return null !== $order && $order->user_id === $user;
} );

// Laravel: routes/channels.php, as for any broadcaster. PrivateChannel( 'orders.42' ) is private-orders.42 here.
Broadcast::channel( 'orders.{id}', fn ( User $user, string $id ) => $user->id === Order::find( $id )?->user_id );
```

**Python**

```python
def may_follow_order(user, channel):
    order = find_order(channel[len("private-orders-"):])
    return order is not None and order.user_id == user

realtime.channel("private-orders-", may_follow_order)
```

- `user` is the asking user's id, or `null` (`None`) for nobody.
- Answer `false` (or `null`) for no, `true` for yes, or for presence `{ id, info }` ([presence](presence.md)).
- In Node the callback may be async. In Python it is a plain function, which may query your database the usual way.
- The longest prefix that fits decides. A private or presence channel nobody owns is refused (`forbidden`).
- The browser asks for every channel it subscribes to in the same moment in one request, and keeps the token across reconnects and moves: your callback runs when a page first subscribes and when its token is renewed (6 hours by default).

## Listening

```ts
const orders = rt.subscribe( 'private-orders-42' );
orders.on( 'updated', ( data ) => refresh( data ) );
orders.on( 'realtime:subscribed', () => console.log( 'listening' ) );
orders.on( 'realtime:error', ( e ) => console.warn( e.code ) );   // forbidden, expired, auth_failed …

rt.unsubscribe( 'private-orders-42' );
```

- `subscribe()` gives the same subscription for the same name.
- Every tab of the browser rides one connection, each with its own subscriptions.
- Events reach a listener opened: you never handle sealed values.
- The page's own events are named `realtime:…`, which nobody can publish.

## Publishing

**Node**

```ts
await realtime.publish( 'private-orders-42', 'updated', { status: 'paid' } );
await realtime.publish( [ 'private-orders-42', 'orders' ], 'updated', { id: 42 } );
await realtime.publish( 'presence-room-7', 'message', { text }, { except: tab } );   // not back to the tab that sent it
```

**PHP**

```php
$realtime->publish( 'private-orders-42', 'updated', [ 'status' => 'paid' ] );
$realtime->publish( [ 'private-orders-42', 'orders' ], 'updated', [ 'id' => 42 ] );
$realtime->publish( 'presence-room-7', 'message', [ 'text' => $text ], [ 'except' => $tab ] );   // not back to the tab that sent it
```

**Python**

```python
realtime.publish("private-orders-42", "updated", {"status": "paid"})
realtime.publish(["private-orders-42", "orders"], "updated", {"id": 42})
realtime.publish("presence-room-7", "message", {"text": text}, except_tab=tab)   # not back to the tab that sent it
```

More in [publishing](publishing.md).

## Client events

Browsers on a private or presence channel can send each other events named `client-…`, sealed like the rest:

```ts
const room = rt.subscribe( 'presence-room-7' );
await room.trigger( 'client-typing', { on: true } );
room.on( 'client-typing', ( data, { user_id } ) => showTyping( user_id, data.on ) );
```

A tab doesn't hear its own. At most 10 a second a browser, 10 KB each. They don't go through your server: check anything that matters on your server instead.

![A chat between Ben and Ann in two browsers: Ann types a reply, and Ben’s browser shows “Ann is typing…” from her client event before her message is sent](/img/apps/chat-typing.webp)
