Real-time chat requires instant message delivery. Laravel Broadcasting with Pusher enables WebSocket communication without server management. At ZIRA Software, we've built chat systems handling thousands of concurrent users.
Setup
Install dependencies:
composer require pusher/pusher-php-server
npm install --save laravel-echo pusher-js
Configure broadcasting:
// 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'),
'useTLS' => true,
],
],
.env:
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your-app-id
PUSHER_APP_KEY=your-app-key
PUSHER_APP_SECRET=your-app-secret
PUSHER_APP_CLUSTER=mt1
Database Schema
Schema::create('conversations', function (Blueprint $table) {
$table->id();
$table->string('name')->nullable();
$table->boolean('is_group')->default(false);
$table->timestamps();
});
Schema::create('conversation_user', function (Blueprint $table) {
$table->foreignId('conversation_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamp('last_read_at')->nullable();
$table->primary(['conversation_id', 'user_id']);
});
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->foreignId('conversation_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->text('body');
$table->timestamps();
});
Models
class Conversation extends Model
{
public function users()
{
return $this->belongsToMany(User::class)
->withPivot('last_read_at');
}
public function messages()
{
return $this->hasMany(Message::class)->latest();
}
public function latestMessage()
{
return $this->hasOne(Message::class)->latest();
}
}
class Message extends Model
{
protected $fillable = ['conversation_id', 'user_id', 'body'];
public function user()
{
return $this->belongsTo(User::class);
}
public function conversation()
{
return $this->belongsTo(Conversation::class);
}
}
Broadcasting Events
// app/Events/MessageSent.php
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class MessageSent implements ShouldBroadcast
{
public $message;
public function __construct(Message $message)
{
$this->message = $message->load('user');
}
public function broadcastOn()
{
return new PresenceChannel('conversation.' . $this->message->conversation_id);
}
public function broadcastWith()
{
return [
'id' => $this->message->id,
'body' => $this->message->body,
'user' => [
'id' => $this->message->user->id,
'name' => $this->message->user->name,
],
'created_at' => $this->message->created_at->toISOString(),
];
}
}
// app/Events/UserTyping.php
class UserTyping implements ShouldBroadcast
{
public $user;
public $conversationId;
public function __construct(User $user, $conversationId)
{
$this->user = $user;
$this->conversationId = $conversationId;
}
public function broadcastOn()
{
return new PresenceChannel('conversation.' . $this->conversationId);
}
public function broadcastAs()
{
return 'user.typing';
}
}
Channel Authorization
// routes/channels.php
Broadcast::channel('conversation.{conversationId}', function ($user, $conversationId) {
$conversation = Conversation::find($conversationId);
if ($conversation && $conversation->users->contains($user)) {
return [
'id' => $user->id,
'name' => $user->name,
];
}
return false;
});
Controller
class MessageController extends Controller
{
public function store(Request $request, Conversation $conversation)
{
$this->authorize('participate', $conversation);
$validated = $request->validate([
'body' => 'required|string|max:1000',
]);
$message = $conversation->messages()->create([
'user_id' => auth()->id(),
'body' => $validated['body'],
]);
broadcast(new MessageSent($message))->toOthers();
return response()->json($message->load('user'));
}
public function typing(Request $request, Conversation $conversation)
{
broadcast(new UserTyping(auth()->user(), $conversation->id))->toOthers();
return response()->json(['status' => 'ok']);
}
}
Frontend (Laravel Echo)
// resources/js/bootstrap.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
forceTLS: true
});
Chat component:
// Chat.vue or React component
const conversationId = 1;
// Join presence channel
const channel = Echo.join(`conversation.${conversationId}`)
.here((users) => {
console.log('Online users:', users);
})
.joining((user) => {
console.log(`${user.name} joined`);
})
.leaving((user) => {
console.log(`${user.name} left`);
})
.listen('MessageSent', (e) => {
messages.push(e);
})
.listenForWhisper('typing', (e) => {
showTypingIndicator(e.user);
});
// Send message
async function sendMessage(body) {
const response = await axios.post(`/conversations/${conversationId}/messages`, {
body
});
messages.push(response.data);
}
// Typing indicator
function handleTyping() {
channel.whisper('typing', {
user: currentUser
});
}
Read Receipts
public function markAsRead(Conversation $conversation)
{
$conversation->users()->updateExistingPivot(auth()->id(), [
'last_read_at' => now(),
]);
broadcast(new MessageRead(auth()->user(), $conversation->id))->toOthers();
return response()->json(['status' => 'ok']);
}
Conclusion
Laravel Broadcasting with Pusher enables real-time chat without WebSocket server management. Presence channels provide online status, whispers handle typing indicators.
Building real-time features? Contact ZIRA Software for chat system development.