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

  1. 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
  1. 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
  1. Update the .env file to use SQLite:
DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/real-time-notifications/database/database.sqlite
  1. Replace /absolute/path/to/ with the actual path to your project directory.
  2. Install Laravel Breeze: Laravel Breeze provides a simple authentication scaffold. Install it with:
composer require laravel/breeze --dev
php artisan breeze:install
  1. 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
  1. Run the migrations to set up the users table:
php artisan migrate
  1. 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
  1. 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');
        });
    }
};
  1. Run the migration:
php artisan migrate
  1. 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
    }
}
  1. Run the seeder:
php artisan db:seed

Step 2: Set Up Laravel Reverb and Pusher

  1. Install Laravel Reverb: If you selected Reverb during the Breeze installation, it’s already installed. If not, run:
php artisan install:broadcasting
  1. 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.
  2. 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).
  3. 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
  1. 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}"
  1. These are example credentials for Reverb. For production, generate unique credentials or use Pusher directly.
  2. 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,
});
  1. 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'],
});
  1. Compile front-end assets: Install Node.js dependencies and compile assets:
npm install
npm run dev

Step 3: Create the Post Model and Event

  1. Create the Post model: Create a Post model with a migration and controller:
php artisan make:model Post -mcr
  1. 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');
    }
};
  1. Run the migration:
php artisan migrate
  1. Create a PostCreated event: Create an event to broadcast when a post is created:
php artisan make:event PostCreated
  1. 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';
    }
}
  1. 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

  1. Create a PostCreatedNotification: Generate a notification class:
php artisan make:notification PostCreatedNotification
  1. 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!');
    }
}
  1. 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

  1. 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
  1. 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
  1. 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

  1. Start the Laravel development server:
php artisan serve
  1. Start the Reverb server (if using Reverb):
php artisan reverb:start
  1. If using Pusher, you don’t need to run the Reverb server, as Pusher handles WebSocket connections.
  2. Compile front-end assets:
npm run dev
  1. Start the queue worker: Broadcasting requires a queue worker to process events:
php artisan queue:work

Step 8: Test the Application

  1. Register a normal user: Visit http://localhost:8000/register and create a regular user account.
  2. Log in as the admin: Log in with admin@example.com and password.
  3. Create a post: Log in as a regular user, navigate to /posts/create, and create a new post.
  4. 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