Hasta Laravel 10, si querías WebSockets en tu aplicación tenías dos caminos: pagar por Pusher o instalar el paquete Laravel WebSockets de Beyondcode, que lleva años sin actualizaciones activas. Ninguna de las dos opciones era ideal.
Con Laravel 11 llegó Reverb, el servidor WebSocket oficial del framework. Corre en tu propio servidor, no tiene costes por mensajes ni por conexiones y no depende de ningún tercero. Además implementa el protocolo de Pusher, lo que significa que puedes usarlo con Laravel Echo sin tocar una sola línea del frontend.
Si ya tenías una app con Echo apuntando a Pusher, cambiar a Reverb es cuestión de actualizar unas variables de entorno.
Instalación y configuración
Instala el paquete con Composer:
composer require laravel/reverb
Después ejecuta el instalador:
php artisan reverb:install
Este comando publica el archivo de configuración config/reverb.php y añade al .env las variables necesarias:
REVERB_APP_ID=mi-app
REVERB_APP_KEY=clave-publica
REVERB_APP_SECRET=clave-secreta
REVERB_HOST=localhost
REVERB_PORT=8080
REVERB_SCHEME=http
Para arrancar el servidor:
php artisan reverb:start
Por defecto escucha en el puerto 8080. En desarrollo puedes dejarlo así, pero en producción necesitas que el proceso se mantenga vivo con Supervisor y que Nginx haga de proxy inverso. Lo vemos en la sección de despliegue.
Una cosa importante: Reverb corre como proceso separado, no dentro de PHP-FPM ni de tu servidor web habitual. Trátalo como tratarías un proceso de Node o cualquier otro daemon.
Events de broadcasting en Laravel
El sistema de broadcasting de Laravel funciona con eventos PHP normales que implementan la interfaz ShouldBroadcast. Cuando disparas uno de estos eventos, Laravel lo manda al servidor WebSocket en lugar de (o además de) procesarlo de forma síncrona.
Crea un evento con Artisan:
php artisan make:event MessageSent
Un evento básico de chat quedaría así:
<?php
namespace AppEvents;
use IlluminateBroadcastingChannel;
use IlluminateBroadcastingInteractsWithSockets;
use IlluminateContractsBroadcastingShouldBroadcast;
use IlluminateFoundationEventsDispatchable;
use IlluminateQueueSerializesModels;
class MessageSent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public string $message,
public string $user
) {}
public function broadcastOn(): array
{
return [new Channel('chat')];
}
public function broadcastAs(): string
{
return 'message.sent';
}
}
broadcastOn() indica en qué canal se emite el evento. broadcastAs() define el nombre con el que llegará al cliente; si no lo defines, Laravel usa el nombre completo de la clase.
Para disparar el evento desde un controlador:
broadcast(new MessageSent($message, $user->name));
Si usas colas, añade ->toOthers() para que el mensaje no se reenvíe al propio emisor:
broadcast(new MessageSent($message, $user->name))->toOthers();
Canales públicos y privados
Laravel tiene tres tipos de canal:
- Channel: público, cualquiera puede escuchar sin autenticarse.
- PrivateChannel: privado, el cliente debe autenticarse antes de suscribirse.
- PresenceChannel: como el privado, pero además el servidor sabe qué usuarios están conectados en cada momento.
Los canales privados y de presencia requieren una ruta de autorización. Se define en routes/channels.php:
use IlluminateSupportFacadesBroadcast;
// Canal privado de chat por sala
Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
return $user->canAccessRoom($roomId);
});
// Canal de presencia
Broadcast::channel('room.{roomId}', function ($user, $roomId) {
if ($user->canAccessRoom($roomId)) {
return ['id' => $user->id, 'name' => $user->name];
}
});
En el canal de presencia, la función de autorización devuelve los datos del usuario que se comparten con el resto de participantes. Si devuelve false o null, el cliente no puede suscribirse.
Laravel Echo en el frontend
Echo es la librería JavaScript que se encarga de la parte cliente. Instálala junto con el driver de Pusher (que Reverb también entiende):
npm install laravel-echo pusher-js
Configura Echo en tu archivo de JS principal (normalmente resources/js/bootstrap.js):
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT,
wssPort: import.meta.env.VITE_REVERB_PORT,
forceTLS: false,
enabledTransports: ['ws', 'wss'],
});
Con esto listo, escuchar eventos en un canal público es una línea:
Echo.channel('chat')
.listen('MessageSent', (e) => {
console.log(e.user + ': ' + e.message);
});
Para un canal privado:
Echo.private('chat.' + roomId)
.listen('.message.sent', (e) => {
addMessageToUI(e);
});
Fíjate en el punto delante del nombre del evento cuando usas broadcastAs() con un nombre personalizado.
Los canales de presencia dan acceso a la lista de usuarios conectados:
Echo.join('room.' + roomId)
.here((users) => {
// Lista inicial de usuarios en la sala
updateUserList(users);
})
.joining((user) => {
addUserToList(user);
})
.leaving((user) => {
removeUserFromList(user);
})
.listen('.message.sent', (e) => {
addMessageToUI(e);
});
Caso práctico: chat en tiempo real
El flujo completo de un mensaje de chat con Reverb funciona así:
- El usuario escribe y envía el formulario.
- Un request llega al controlador de Laravel.
- El controlador guarda el mensaje en la base de datos.
- Se dispara el evento
MessageSent. - Reverb lo recibe y lo transmite a todos los clientes suscritos al canal.
- Echo lo captura y actualiza la interfaz.
No hay polling, no hay setTimeout preguntando cada dos segundos si hay mensajes nuevos. El evento llega en milisegundos.
Un controlador mínimo para esto:
public function store(Request $request, Room $room)
{
$message = $room->messages()->create([
'user_id' => auth()->id(),
'body' => $request->input('body'),
]);
broadcast(new MessageSent(
$message->body,
auth()->user()->name
))->toOthers();
return response()->json($message);
}
En el frontend puedes usar Alpine.js para manejar el estado local del chat y añadir los mensajes entrantes al array sin recargar la página. La combinación de Livewire + Echo también funciona muy bien si ya usas Livewire en el proyecto: hay un trait WithBroadcasting que simplifica la integración.
Si quieres ver cómo encaja esto en una arquitectura mayor, échale un ojo a cómo estructurar una aplicación Laravel lista para tiempo real. Y si necesitas un panel de administración que refleje datos en vivo, FilamentPHP para paneles admin con datos en tiempo real es buena lectura complementaria.
Despliegue en producción
En producción hay tres cosas que configurar: Supervisor para el proceso, Nginx como proxy WebSocket y los certificados TLS.
Supervisor
Crea un archivo de configuración para Supervisor, por ejemplo /etc/supervisor/conf.d/reverb.conf:
[program:reverb]
command=php /var/www/tu-app/artisan reverb:start --host=0.0.0.0 --port=8080
directory=/var/www/tu-app
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/reverb.log
Después:
supervisorctl reread
supervisorctl update
supervisorctl start reverb
Nginx como proxy WebSocket
Para que las conexiones WebSocket pasen por HTTPS (puerto 443) y lleguen a Reverb (puerto 8080), añade esto al bloque server de tu dominio:
location /app {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 60;
}
Con esto las conexiones van cifradas y no necesitas abrir el puerto 8080 al exterior. En el .env cambia REVERB_SCHEME=https y ajusta los puertos en la configuración de Echo.
Monitorización
Para ver qué ocurre en tiempo real con las conexiones, arranca Reverb en modo debug:
php artisan reverb:start --debug
Verás cada conexión, desconexión y mensaje que pasa por el servidor. Útil para detectar problemas de autorización en canales privados o clientes que no se conectan bien.
Reverb expone también métricas internas que puedes consultar mediante el comando reverb:connections para saber cuántas conexiones activas hay en un momento dado.
Imagen: Pexels / Muhammed Ensar
