In Laravel broadcasting, how are private and presence channels authorized with Broadcast::channel, and what must a presence channel callback return?
answer
- routes/channels.php callbacks
- Echo calls /broadcasting/auth
- user first, wildcards after
- private: true or false
- presence: an array of user data
basics
~20 sEcho asks /broadcasting/auth before subscribing; Laravel runs the matching Broadcast::channel callback from routes/channels.php with the authenticated user and wildcard values. Private callbacks return true or false; presence callbacks return an array describing the user, or false/null to refuse.
solid answer
~40 sWhen Echo subscribes to `private-auctions.42` or `presence-auctions.42`, it first sends the socket ID and channel name to `/broadcasting/auth`, a route registered in the `web` middleware group. Laravel strips the prefix and finds the `Broadcast::channel('auctions.{auction}', ...)` callback in `routes/channels.php`. The callback receives the user from the default guard (or the `guards` option) and the wildcard values, which can use model binding. For a **private** channel it returns `true` to allow or `false` to deny, and a denial is a 403. For a **presence** channel it returns an **array** such as `['id' => $user->id, 'name' => $user->name]`; that data is what other members see in Echo's `here`, `joining` and `leaving` callbacks. A guest is refused before any callback runs, and `php artisan channel:list` lists the callbacks.
code
javascript · 5 linesEcho.join(`auction-room.${auctionId}`)
.here((users) => setWatchers(users))
.joining((user) => addWatcher(user))
.leaving((user) => removeWatcher(user))
.error((error) => console.error(error));go deeper
Recall that routes/channels.php holds Broadcast::channel callbacks and that private and presence channels need them.
Explain the /broadcasting/auth round trip, the callback arguments with model binding and guards, and why presence callbacks return arrays.
Treat presence data as published to every member, choose private versus presence per feature, and keep channel rules in sync with the app's policies.
Set rules for what may ever travel over realtime channels and how channel authorization reuses the app's policies instead of duplicating them.
## Why channels need authorization A **public channel** (`Channel`) can be subscribed to by anyone who knows its name. **Private** (`PrivateChannel`) and **presence** (`PresenceChannel`) channels are for data a visitor must be entitled to: the outbid notice for one bidder, or the room of people watching an invite-only auction. The WebSocket server cannot decide who is entitled, so it asks the Laravel application to sign each subscription. ## The authorization round trip 1. Echo connects to the WebSocket server and receives a **socket ID**. 2. To subscribe to `private-auctions.42`, Echo sends an HTTP request to `/broadcasting/auth` with the socket ID and the channel name. The request carries the session cookie, so it is authenticated like any web request. 3. Laravel removes the `private-` or `presence-` prefix and matches the rest against the patterns registered with `Broadcast::channel()`. 4. If the callback allows it, Laravel returns a signature the WebSocket server verifies; otherwise it responds with **403** and Echo reports an error. The `/broadcasting/auth` route is registered when `routes/channels.php` is wired into `withRouting()` (done by `install:broadcasting`). By default it sits in the `web` middleware group and is exempt from the CSRF check. ## Writing the callbacks Callbacks live in `routes/channels.php`: - the pattern uses `{placeholders}` like a route: `'auctions.{auction}'` - the **first argument** is the authenticated user; the placeholders follow in order - placeholders can use **model binding**, so `Auction $auction` receives the model (implicit binding scoping is not supported here) - the user is resolved through the default guard, or the guards listed in a third argument, `['guards' => ['web', 'admin']]` - if no user is authenticated, the request is refused before the callback runs - large apps can move the logic into a channel class created with `php artisan make:channel AuctionChannel`, whose `join()` method has the same contract ## Private versus presence return values | Channel type | Callback returns to allow | To deny | Extra effect | |---|---|---|---| | `PrivateChannel` | `true` (any truthy value) | `false` | none | | `PresenceChannel` | an array of user info | `false` or `null` | the array is shared with other members | A callback that returns `null` for a private channel is also treated as a refusal, because only a truthy result is accepted. For a presence channel the returned array is sent to every member. Echo's `join()` exposes it: - `here(users)` runs once after joining with the current members - `joining(user)` and `leaving(user)` run as others come and go So the array should contain only what other members may see, such as an ID and a display name, never an email address or role flags. ## The auction example ```php use App\Models\Auction; use App\Models\User; use Illuminate\Support\Facades\Broadcast; Broadcast::channel('bidders.{bidder}', fn (User $user, User $bidder) => $user->is($bidder)); Broadcast::channel('auction-room.{auction}', function (User $user, Auction $auction) { if ($user->can('view', $auction)) { return ['id' => $user->id, 'name' => $user->display_name]; } return false; }); ``` The first gives each bidder a private channel for "you have been outbid" messages. The second is a presence channel showing who is watching an invite-only auction, and it reuses the auction policy instead of duplicating the rule. ## Debugging a refused subscription A 403 from `/broadcasting/auth` in the browser's network tab means Laravel ran the lookup and said no. The usual causes are: - the user is not authenticated on the guard the channel checks, for example a user logged in on an `admin` guard while the channel uses the default one - the pattern in `Broadcast::channel()` does not match the subscribed name, often because of a typo or a stray `private-` prefix - a model-bound placeholder did not resolve, or the callback's rule is simply false for this user - the callback returned `null` for a private channel, which counts as a refusal `php artisan channel:list` shows every registered pattern and its handler, which settles the pattern question quickly. ## Frequent mistakes - returning `true` from a presence callback, which gives other members nothing to identify the user by - putting the channel name with its `private-` prefix into `Broadcast::channel()` - assuming a channel is protected because its name is hard to guess, while broadcasting it as a public `Channel` - returning the whole `User` model from a presence callback and leaking its attributes to every member
- What happens when a guest's Echo client tries to subscribe to a private channel?The `/broadcasting/auth` request has no authenticated user, so Laravel refuses it with a 403 before running any `Broadcast::channel` callback. Echo reports a subscription error and the client receives nothing on that channel.
- How do you authorize a channel for users logged in through an admin guard?Pass the guards as the third argument: `Broadcast::channel('auction-admin.{auction}', $callback, ['guards' => ['web', 'admin']])`. Laravel tries each guard to find the user before running the callback; without the option only the default guard is used.
- routes/channels.php has grown to dozens of closures; how do you organise it?Generate channel classes with `php artisan make:channel AuctionChannel`, put the rule in the class's `join()` method, and register it with `Broadcast::channel('auction-room.{auction}', AuctionChannel::class)`. `join()` takes the same arguments and returns the same values as a closure, and the class is resolved from the container, so it can receive dependencies such as a policy service.
saying these in an interview costs you the question
- A presence channel callback should return true, like a private channel.
- Channel patterns in Broadcast::channel must include the private- prefix.
- The WebSocket server decides who may join a private channel on its own.
- Returning the whole User model from a presence callback is harmless.
- Presence channels need no Broadcast::channel callback because members are public.