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)