Implementing Real-Time Notifications in Laravel with Laravel Reverb and Pusher
Real-time notifications enhance user experience by delivering instant updates without requiring page refreshes. In this article, we'll walk through the complete process of implementing real-time notifications in a Laravel 11 application using Laravel Reverb (a first-party WebSocket server for Laravel) and Pusher (a cloud-based WebSocket service). We'll create a simple application where an admin user receives real-time notifications when a new post is created by a user.
This guide assumes you have a basic understanding of Laravel, PHP, and JavaScript, and have Node.js, Composer, and a database (e.g., MySQL or SQLite) installed.
Prerequisites
- Laravel 11 installed
- Node.js and npm for front-end dependencies
- Composer for PHP dependencies
- A Pusher account (sign up at pusher.com) for Pusher integration
- A local development environment (e.g., Laravel Valet, Laravel Sail, or XAMPP)
- Basic knowledge of Laravel's event broadcasting and notifications
Step 1: Set Up a New Laravel Project
- Create a new Laravel project: Run the following command to create a fresh Laravel application:
composer create-project laravel/laravel real-time-notifications cd real-time-notifications
- Set up the database: For simplicity, we'll use SQLite as the database. Create a database.sqlite file in the database directory:
touch database/database.sqlite
- Update the .env file to use SQLite:
DB_CONNECTION=sqlite DB_DATABASE=/absolute/path/to/real-time-notifications/database/database.sqlite
- Replace /absolute/path/to/ with the actual path to your project directory.
- Install Laravel Breeze: Laravel Breeze provides a simple authentication scaffold. Install it with:
composer require laravel/breeze --dev php artisan breeze:install
- Choose the following options during installation:
- Blade for the stack
- Yes for SSR (Server-Side Rendering, optional)
- Yes for Reverb broadcasting
- SQLite as the database
- Run the migrations to set up the users table:
php artisan migrate
- Add an admin column: Modify the users table to include an is_admin column to distinguish admin users. Create a migration:
php artisan make:migration add_is_admin_to_users_table
- Edit the generated migration file (database/migrations/YYYY_MM_DD_add_is_admin_to_users_table.php):
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false);
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
});
}
};
- Run the migration:
php artisan migrate
- Seed an admin user: Modify the database/seeders/DatabaseSeeder.php to create an admin user:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
User::create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => bcrypt('password'),
'is_admin' => true,
]);
User::factory(5)->create(); // Create 5 regular users
}
}
- Run the seeder:
php artisan db:seed
Step 2: Set Up Laravel Reverb and Pusher
- Install Laravel Reverb: If you selected Reverb during the Breeze installation, it’s already installed. If not, run:
php artisan install:broadcasting
- Choose Reverb as the broadcasting driver. This command installs laravel-echo and pusher-js and creates the config/broadcasting.php and resources/js/echo.js files.
- Configure Pusher: Sign up for a free Pusher account at pusher.com. Create a new Channels app and note down the credentials (App ID, Key, Secret, and Cluster).
- Update the .env file with Pusher credentials:
BROADCAST_CONNECTION=pusher PUSHER_APP_ID=your-app-id PUSHER_APP_KEY=your-app-key PUSHER_APP_SECRET=your-app-secret PUSHER_APP_CLUSTER=your-app-cluster
- For Reverb, add the following to your .env file:
REVERB_APP_ID=256980
REVERB_APP_KEY=f4l2tmwqf6eg0f6jz0mw
REVERB_APP_SECRET=zioqeto9xrytlnlg7sj6
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
- These are example credentials for Reverb. For production, generate unique credentials or use Pusher directly.
- Configure Laravel Echo: The install:broadcasting command creates resources/js/echo.js. Update it to use Pusher:
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
forceTLS: true,
});
- Alternatively, if using Reverb, configure it as follows:
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 ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
- Compile front-end assets: Install Node.js dependencies and compile assets:
npm install npm run dev
Step 3: Create the Post Model and Event
- Create the Post model: Create a Post model with a migration and controller:
php artisan make:model Post -mcr
- Edit the migration file (database/migrations/YYYY_MM_DD_create_posts_table.php):
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
- Run the migration:
php artisan migrate
- Create a PostCreated event: Create an event to broadcast when a post is created:
php artisan make:event PostCreated
- Edit app/Events/PostCreated.php:
<?php
namespace App\Events;
use App\Models\Post;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PostCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $post;
public function __construct(Post $post)
{
$this->post = $post;
}
public function broadcastOn(): array
{
return [
new PrivateChannel('admin-notifications'),
];
}
public function broadcastAs(): string
{
return 'post.created';
}
}
- Authorize the channel: Update routes/channels.php to authorize the admin channel:
<?php
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('admin-notifications', function (User $user) {
return $user->is_admin;
});
Step 4: Create a Notification
- Create a PostCreatedNotification: Generate a notification class:
php artisan make:notification PostCreatedNotification
- Edit app/Notifications/PostCreatedNotification.php:
<?php
namespace App\Notifications;
use App\Models\Post;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;
class PostCreatedNotification extends Notification
{
use Queueable;
public $post;
public function __construct(Post $post)
{
$this->post
= $post; }
public function via(object $notifiable): array
{
return ['broadcast'];
}
public function toBroadcast(object $notifiable): BroadcastMessage
{
return new BroadcastMessage([
'message' => "New post created: {$this->post->title} by {$this->post->user->name}",
'post_id' => $this->post->id,
]);
}
}
---
## Step 5: Set Up the Post Controller and Routes
1. **Update the PostController**:
Edit `app/Http/Controllers/PostController.php` to handle post creation and dispatch the event:
```php
<?php
namespace App\Http\Controllers;
use App\Events\PostCreated;
use App\Models\Post;
use App\Models\User;
use App\Notifications\PostCreatedNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Notification;
class PostController extends Controller
{
public function index()
{
return view('posts.index', ['posts' => Post::all()]);
}
public function create()
{
return view('posts.create');
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
]);
$post = Post::create([
'title' => $request->title,
'body' => $request->body,
'user_id' => auth()->id(),
]);
// Dispatch event
event(new PostCreated($post));
// Notify admins
$admins = User::where('is_admin', true)->get();
Notification::send($admins, new PostCreatedNotification($post));
return redirect()->route('posts.index')->with('success', 'Post created successfully!');
}
}
- Define routes: Update routes/web.php:
<?php
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Auth::routes();
Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create');
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
Step 6: Create Views
- Create the posts index view: Create resources/views/posts/index.blade.php:
@extends('layouts.app')
@section('content')
<div class="container">
<h1>Posts</h1>
<a href="{{ route('posts.create') }}" class="btn btn-primary mb-3">Create Post</a>
<div id="notifications" class="alert alert-info" style="display: none;"></div>
<ul class="list-group">
@foreach ($posts as $post)
<li class="list-group-item">{{ $post->title }} by {{ $post->user->name }}</li>
@endforeach
</ul>
</div>
@endsection
@section('script')
<script>
Echo.private('admin-notifications')
.listen('.post.created', (e) => {
const notificationDiv = document.getElementById('notifications');
notificationDiv.style.display = 'block';
notificationDiv.innerText = e.message;
setTimeout(() => {
notificationDiv.style.display = 'none';
}, 5000);
});
</script>
@endsection
- Create the post creation view: Create resources/views/posts/create.blade.php:
@extends('layouts.app')
@section('content')
<div class="container">
<h1>Create Post</h1>
<form method="POST" action="{{ route('posts.store') }}">
@csrf
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="body" class="form-label">Body</label>
<textarea class="form-control" id="body" name="body" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
@endsection
- Update the main layout: Ensure resources/views/layouts/app.blade.php includes the necessary scripts. The Breeze installation should have set this up, but verify that @vite(['resources/sass/app.scss', 'resources/js/app.js']) is included in the <head> section.
Step 7: Start the Servers
- Start the Laravel development server:
php artisan serve
- Start the Reverb server (if using Reverb):
php artisan reverb:start
- If using Pusher, you don’t need to run the Reverb server, as Pusher handles WebSocket connections.
- Compile front-end assets:
npm run dev
- Start the queue worker: Broadcasting requires a queue worker to process events:
php artisan queue:work
Step 8: Test the Application
- Register a normal user: Visit http://localhost:8000/register and create a regular user account.
- Log in as the admin: Log in with admin@example.com and password.
- Create a post: Log in as a regular user, navigate to /posts/create, and create a new post.
- Check the notification: As the admin user, visit /posts. When a new post is created by any user, you should see a notification appear in real-time on the page (e.g., "New post created: [Title] by [User]").
Step 9: Optional Enhancements
- Styling: Use Tailwind CSS (included with Breeze) to improve the UI of the notification and post forms.
- Multiple Channels: Extend the application to support multiple notification types or channels (e.g., public channels for all users).
- Queue Optimization: Configure a more robust queue driver (e.g., Redis) for production.
- Security: Ensure proper authentication and authorization for private channels in production.
- Novu Integration: For a more comprehensive notification system, consider integrating Novu for multi-channel notifications (e.g., email, SMS) alongside real-time WebSocket notifications.
Troubleshooting
- No notifications received: Ensure the Reverb server or Pusher is running, and check the browser console for WebSocket errors.
- Channel authorization issues: Verify the channels.php configuration and ensure the user is authorized for the admin-notifications channel.
- Queue issues: Ensure the queue worker is running (php artisan queue:work) and the QUEUE_CONNECTION in .env is set to a valid driver (e.g., database or redis).
Conclusion
In this article, we implemented a real-time notification system in Laravel 11 using Laravel Reverb and Pusher. We created a simple application where admins receive instant notifications when users create posts. This setup leverages Laravel’s broadcasting system, Laravel Echo, and WebSocket technology to deliver a seamless user experience.
For further reading, check the official documentation:
- Laravel Reverb
- Laravel Broadcasting
- Pusher Channels
Comments (0)