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.
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
-
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).## ConclusionIn 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
Top comments (0)