ffrir des fonctionnalités interactives instantanées (messagerie en direct, notifications temps réel, indicateurs de présence, mise à jour instantanée d'un tableau de bord de trading ou d'e-learning) est devenu un standard incontournable de l'expérience utilisateur moderne.
Historiquement, la mise en œuvre de WebSockets sous PHP imposait de recourir à des services tiers onéreux (Pusher) ou à des passerelles complexes en Node.js (Socket.io).
Avec l'arrivée de Laravel Reverb, PHP dispose désormais d'un serveur WebSocket natif, ultra-rapide, capable de maintenir des dizaines de milliers de connexions simultanées avec une consommation de ressources minimale.
Découvrons comment interconnecter Laravel Reverb et Nuxt 4 derrière un proxy inverse Traefik.
1. Comment Fonctionne l'Architecture Reverb
Reverb s'exécute comme un démon autonome asynchrone propulsé par une boucle d'événements interne. Il communique avec vos travailleurs d'arrière-plan via Redis pour diffuser (broadcast) instantanément les événements vers les clients connectés.
Le Cycle d'un Événement Temps Réel
Une action utilisateur survient sur l'API (ex: nouveau commentaire soumis).
L'API déclenche un événement Laravel implémentant le contrat
ShouldBroadcastNowouShouldBroadcast.Le message est publié dans un canal Redis Pub/Sub.
Reverb capte le message et le pousse immédiatement dans le socket de tous les navigateurs connectés et autorisés.
Le composant Vue.js met à jour son état réactif en quelques millisecondes sans rechargement de page.
2. Configuration du Routage WebSocket sous Traefik
Pour que les flux de données persistants transitent sans coupure à travers Traefik en HTTPS (wss://), Traefik doit être configuré pour reconnaître la négociation de protocole (HTTP Upgrade).
Configuration Docker Compose pour Reverb
services:
reverb:
image: mon-api:latest
restart: unless-stopped
command: ["php", "artisan", "reverb:start", "--host=0.0.0.0", "--port=8080"]
environment:
REVERB_APP_ID: mon-app-reverb
REVERB_APP_KEY: cle-reverb-securisee-987452
REVERB_APP_SECRET: secret-reverb-ultra-securise
REVERB_HOST: 0.0.0.0
REVERB_PORT: 8080
REVERB_SCHEME: https
networks:
- dokploy-network
- inte
al-net
labels:
- "traefik.enable=true"
- "traefik.docker.network=dokploy-network"
- "traefik.http.routers.reverb.rule=Host(`api.domaine.com`) && PathPrefix(`/app`)"
- "traefik.http.routers.reverb.entrypoints=websecure"
- "traefik.http.routers.reverb.tls=true"
- "traefik.http.routers.reverb.tls.certresolver=letsencrypt"
- "traefik.http.services.reverb.loadbalancer.server.port=8080"3. Déclaration d'un Événement Diffusé (Broadcast Event)
Dans Laravel, la création d'un événement diffusable nécessite l'implémentation de ShouldBroadcastNow (pour une diffusion immédiate) ou ShouldBroadcast (pour un passage par la file d'attente) :
namespace App\Events;
use App\Models\Comment;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
class NewCommentEvent implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets;
public function __construct(public Comment $comment) {}
/**
* Canal public sur lequel l'événement est diffusé.
*/
public function broadcastOn(): array
{
return [
new Channel('posts.' . $this->comment->post_id),
];
}
/**
* Nom d'événement personnalisé reçu par le frontend.
*/
public function broadcastAs(): string
{
return 'comment.created';
}
/**
* Données précises injectées dans le payload WebSocket.
*/
public function broadcastWith(): array
{
return [
'id' => $this->comment->id,
'content' => $this->comment->content,
'author_name' => $this->comment->user->name,
'created_at' => $this->comment->created_at->toISOString(),
];
}
}4. Intégration Client dans Nuxt 4 avec Laravel Echo
Côté frontend, nous exploitons la combinaison de laravel-echo et du driver pusher-js (pleinement compatible avec Reverb).
Plugin d'Initialisation Nuxt 4 (app/plugins/echo.client.ts)
import Echo from 'laravel-echo'
import Pusher from 'pusher-js'
declare global {
interface Window {
Pusher: typeof Pusher
Echo: Echo<any>
}
}
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
if (import.meta.server) return
window.Pusher = Pusher
const echo = new Echo({
broadcaster: 'reverb',
key: config.public.reverbAppKey,
wsHost: config.public.reverbHost,
wsPort: Number(config.public.reverbPort) || 443,
wssPort: Number(config.public.reverbPort) || 443,
forceTLS: config.public.reverbScheme === 'https',
enabledTransports: ['ws', 'wss'],
})
return {
provide: {
echo
}
}
})Écoute Réactive dans un Composant Vue 3
<script setup lang="ts">
const props = defineProps<{ postId: number }>()
const comments = ref<any[]>([])
const { $echo } = useNuxtApp()
onMounted(() => {
// Souscription au canal dynamique du post
$echo.channel(`posts.${props.postId}`)
.listen('.comment.created', (event: any) => {
// Ajout instantané du commentaire en tête de liste
comments.value.unshift(event)
})
})
onUnmounted(() => {
// Nettoyage impératif pour éviter les écoutes fantômes en mémoire
$echo.leaveChannel(`posts.${props.postId}`)
})
</script>
<template>
<div class="space-y-4">
<div
v-for="c in comments"
:key="c.id"
class="p-4 rounded-xl border border-muted bg-card shadow-sm transition-all animate-in fade-in slide-in-from-top-2"
>
<div class="flex items-center justify-between text-xs text-muted-foreground mb-1">
<span class="font-bold">{{ c.author_name }}</span>
<span>{{ new Date(c.created_at).toLocaleTimeString() }}</span>
</div>
<p class="text-sm">{{ c.content }}</p>
</div>
</div>
</template>5. Bonnes Pratiques de Production pour Reverb
Ping / Pong Interval : Réglez l'intervalle de pulsation (Heartbeat) à 25 ou 30 secondes pour que les routeurs et pare-feu intermédiaires ne ferment pas prématurément les connexions inactives.
Autorisation des Canaux Privés : Pour les canaux privés (
PrivateChannel) et de présence (PresenceChannel), assurez-vous que la route/broadcasting/authest déclarée dans vosSANCTUM_STATEFUL_DOMAINSpour valider les permissions utilisateur via le cookie de session sans régénérer de jeton.Nettoyer les écouteurs à la destruction du composant : Appelez toujours
$echo.leaveChannel()dansonUnmounted()pour éviter les fuites de mémoire dans les applications Single-Page.




