Users expect real-time updates. Laravel's event broadcasting with Pusher makes it simple to add live notifications, chat, dashboards, and collaborative features. At ZIRA Software, we've used this stack to build everything from live dashboards to multiplayer applications.
Why Real-Time Features?
User expectations:
- Instant notifications
- Live chat
- Real-time dashboards
- Collaborative editing
- Activity feeds
Technical benefits:
- No polling required
- Reduced server load
- Better user experience
- Scalable architecture
What is Pusher?
Pusher is a hosted service that handles WebSocket connections for you:
- No WebSocket server management
- Global infrastructure
- SDKs for all platforms
- Free tier for development
Alternatives:
- Socket.io (self-hosted)
- Laravel WebSockets (self-hosted)
- Firebase Realtime Database
- Ably
Setting Up Pusher
1. Create Pusher Account
Sign up at pusher.com and create an app. Note your credentials:
- App ID
- Key
- Secret
- Cluster
2. Install Pusher PHP SDK
composer require pusher/pusher-php-server
3. Configure Laravel
# .env
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your-app-id
PUSHER_APP_KEY=your-key
PUSHER_APP_SECRET=your-secret
PUSHER_APP_CLUSTER=mt1
// config/broadcasting.php
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'encrypted' => true,
],
],
4. Install Frontend Dependencies
npm install --save laravel-echo pusher-js
Creating Broadcastable Events
Generate Event
php artisan make:event OrderShipped
Implement ShouldBroadcast
<?php namespace App\Events;
use App\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class OrderShipped implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
/**
* Get the channels the event should broadcast on.
*/
public function broadcastOn()
{
return new Channel('orders');
}
/**
* Data to broadcast
*/
public function broadcastWith()
{
return [
'order_id' => $this->order->id,
'customer' => $this->order->customer->name,
'total' => $this->order->total,
'shipped_at' => $this->order->shipped_at
];
}
}
Fire the Event
use App\Events\OrderShipped;
public function ship(Order $order)
{
$order->update(['status' => 'shipped', 'shipped_at' => now()]);
event(new OrderShipped($order));
return response()->json(['message' => 'Order shipped']);
}
Frontend Integration
Configure Laravel Echo
// resources/assets/js/bootstrap.js
import Echo from "laravel-echo"
window.Pusher = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
encrypted: true
});
Listen to Events
// Listen on public channel
Echo.channel('orders')
.listen('OrderShipped', (e) => {
console.log('Order shipped:', e.order_id);
// Update UI
$('#order-' + e.order_id).addClass('shipped');
// Show notification
toastr.success(`Order #${e.order_id} has been shipped!`);
});
Private Channels
Broadcast to specific users:
Define Private Channel
public function broadcastOn()
{
return new PrivateChannel('user.' . $this->user->id);
}
Authorize Channel
// routes/channels.php
Broadcast::channel('user.{userId}', function ($user, $userId) {
return (int) $user->id === (int) $userId;
});
Listen on Frontend
// Must be authenticated
Echo.private(`user.${userId}`)
.listen('MessageReceived', (e) => {
appendMessage(e.message);
});
Presence Channels
Track who's online:
Create Presence Channel Event
public function broadcastOn()
{
return new PresenceChannel('chat');
}
Authorize Channel
Broadcast::channel('chat', function ($user) {
return [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar_url
];
});
Listen for Join/Leave
Echo.join('chat')
.here((users) => {
// Users currently in channel
console.log('Online users:', users);
displayOnlineUsers(users);
})
.joining((user) => {
// User joined
console.log(user.name + ' joined');
addUserToList(user);
})
.leaving((user) => {
// User left
console.log(user.name + ' left');
removeUserFromList(user);
})
.listen('MessageSent', (e) => {
// Listen for events too
appendMessage(e.message);
});
Real-World Examples
1. Live Notifications
Backend:
// App/Events/NotificationSent.php
class NotificationSent implements ShouldBroadcast
{
public $notification;
public function __construct(Notification $notification)
{
$this->notification = $notification;
}
public function broadcastOn()
{
return new PrivateChannel('user.' . $this->notification->user_id);
}
public function broadcastWith()
{
return [
'id' => $this->notification->id,
'type' => $this->notification->type,
'message' => $this->notification->message,
'url' => $this->notification->url,
'created_at' => $this->notification->created_at->toISOString()
];
}
}
// Fire event
event(new NotificationSent($notification));
Frontend:
Echo.private(`user.${userId}`)
.listen('NotificationSent', (e) => {
// Increment badge
let count = parseInt($('#notification-count').text()) + 1;
$('#notification-count').text(count).show();
// Add to dropdown
$('#notifications-list').prepend(`
<li>
<a href="${e.url}">
<i class="fa fa-${e.type}"></i>
${e.message}
<span class="time">${moment(e.created_at).fromNow()}</span>
</a>
</li>
`);
// Show toast
toastr.info(e.message);
// Play sound
new Audio('/sounds/notification.mp3').play();
});
2. Live Chat
Backend:
// App/Events/MessageSent.php
class MessageSent implements ShouldBroadcast
{
public $message;
public function __construct(Message $message)
{
$this->message = $message;
}
public function broadcastOn()
{
return new PresenceChannel('chat.' . $this->message->chat_room_id);
}
public function broadcastWith()
{
return [
'id' => $this->message->id,
'user' => [
'id' => $this->message->user->id,
'name' => $this->message->user->name,
'avatar' => $this->message->user->avatar_url
],
'body' => $this->message->body,
'created_at' => $this->message->created_at->toISOString()
];
}
}
// Controller
public function send(Request $request, $roomId)
{
$message = Message::create([
'chat_room_id' => $roomId,
'user_id' => auth()->id(),
'body' => $request->body
]);
event(new MessageSent($message));
return response()->json($message);
}
Frontend:
const roomId = $('#chat-room').data('room-id');
Echo.join(`chat.${roomId}`)
.here((users) => {
displayOnlineUsers(users);
})
.joining((user) => {
addOnlineUser(user);
appendSystemMessage(`${user.name} joined the chat`);
})
.leaving((user) => {
removeOnlineUser(user);
appendSystemMessage(`${user.name} left the chat`);
})
.listen('MessageSent', (e) => {
appendMessage(e);
scrollToBottom();
});
// Send message
$('#message-form').on('submit', function(e) {
e.preventDefault();
const body = $('#message-input').val();
$.post(`/chat/rooms/${roomId}/messages`, { body })
.done(() => {
$('#message-input').val('');
});
});
function appendMessage(data) {
const isOwnMessage = data.user.id === currentUserId;
$('#messages').append(`
<div class="message ${isOwnMessage ? 'own' : ''}">
<img src="${data.user.avatar}" class="avatar">
<div class="content">
<div class="user">${data.user.name}</div>
<div class="body">${data.body}</div>
<div class="time">${moment(data.created_at).format('HH:mm')}</div>
</div>
</div>
`);
}
3. Live Dashboard
Backend:
// App/Events/MetricsUpdated.php
class MetricsUpdated implements ShouldBroadcast
{
public $metrics;
public function __construct(array $metrics)
{
$this->metrics = $metrics;
}
public function broadcastOn()
{
return new Channel('dashboard');
}
}
// Scheduled command
protected function schedule(Schedule $schedule)
{
$schedule->call(function() {
$metrics = [
'users_online' => User::where('last_active_at', '>', now()->subMinutes(5))->count(),
'orders_today' => Order::whereDate('created_at', today())->count(),
'revenue_today' => Order::whereDate('created_at', today())->sum('total'),
'server_load' => sys_getloadavg()[0]
];
event(new MetricsUpdated($metrics));
})->everyMinute();
}
Frontend:
Echo.channel('dashboard')
.listen('MetricsUpdated', (e) => {
// Update metrics
$('#users-online').text(e.metrics.users_online);
$('#orders-today').text(e.metrics.orders_today);
$('#revenue-today').text('$' + parseFloat(e.metrics.revenue_today).toFixed(2));
// Update chart
updateRevenueChart(e.metrics);
// Update server load indicator
const loadClass = e.metrics.server_load > 2 ? 'danger' : 'success';
$('#server-load').removeClass().addClass('badge badge-' + loadClass)
.text(e.metrics.server_load.toFixed(2));
});
4. Typing Indicator
Backend:
// App/Events/UserTyping.php
class UserTyping implements ShouldBroadcast
{
public $user;
public $chatRoomId;
public function __construct(User $user, $chatRoomId)
{
$this->user = $user;
$this->chatRoomId = $chatRoomId;
}
public function broadcastOn()
{
return new PresenceChannel('chat.' . $this->chatRoomId);
}
public function broadcastWith()
{
return [
'user_id' => $this->user->id,
'user_name' => $this->user->name
];
}
}
// Endpoint
public function typing($roomId)
{
event(new UserTyping(auth()->user(), $roomId));
return response()->json(['status' => 'ok']);
}
Frontend:
let typingTimer;
$('#message-input').on('keyup', function() {
clearTimeout(typingTimer);
$.post(`/chat/rooms/${roomId}/typing`);
typingTimer = setTimeout(() => {
// Stop showing typing after 3 seconds of inactivity
}, 3000);
});
Echo.join(`chat.${roomId}`)
.listen('UserTyping', (e) => {
if (e.user_id !== currentUserId) {
showTypingIndicator(e.user_name);
setTimeout(() => {
hideTypingIndicator(e.user_name);
}, 3000);
}
});
Queue Broadcasting
For better performance, queue broadcasts:
class OrderShipped implements ShouldBroadcast
{
use InteractsWithBroadcasting;
public function __construct(Order $order)
{
$this->order = $order;
// Delay broadcast by 5 seconds
$this->broadcastVia('queue')->delay(now()->addSeconds(5));
}
}
Client Events
Broadcast from client without hitting server:
Echo.join('chat.1')
.whisper('typing', {
name: currentUser.name
});
// Listen
Echo.join('chat.1')
.listenForWhisper('typing', (e) => {
showTypingIndicator(e.name);
});
Debugging
Enable debug mode:
window.Echo = new Echo({
broadcaster: 'pusher',
key: key,
cluster: cluster,
encrypted: true,
enabledTransports: ['ws', 'wss'],
wsHost: window.location.hostname,
wsPort: 6001,
forceTLS: false,
disableStats: true,
enableLogging: true // Enable logging
});
Check Pusher Debug Console: Visit your Pusher dashboard to see real-time events
Best Practices
- Queue broadcasts - Don't block requests
- Broadcast only necessary data - Keep payloads small
- Use private channels for sensitive data
- Handle disconnections gracefully
- Test with multiple users
- Monitor Pusher usage - Stay within limits
- Consider self-hosted alternatives for high volume
Conclusion
Real-time features transform user experience. Laravel's event broadcasting with Pusher makes implementation straightforward, letting you focus on features instead of infrastructure. At ZIRA Software, we've shipped dozens of real-time applications using this stack.
Whether you're building chat, notifications, or live dashboards, Laravel + Pusher provides a solid foundation for real-time web applications.
Need real-time features in your application? Contact ZIRA Software to discuss WebSocket implementation, real-time architecture, and building engaging user experiences.