Laravel Integration Guide

Vask is a drop-in Pusher replacement for Laravel Broadcasting and Laravel Echo. The fastest way in is the vask/laravel Composer package — it OAuths into your account, writes the PUSHER_* credentials to .env, and verifies the connection.

#Installation

composer require vask/laravel
php artisan vask:install

What vask:install does:

  • Runs the OAuth device flow (you approve a short code in your browser — no git config or local tokens).
  • Writes PUSHER_APP_ID, PUSHER_APP_KEY, PUSHER_APP_SECRET, PUSHER_HOST, PUSHER_PORT, PUSHER_SCHEME, and PUSHER_APP_CLUSTER to your .env.
  • Runs vask:doctor afterwards to confirm everything connects.

Source: github.com/vask-dev/laravel · packagist.org/packages/vask/laravel.

#Manual install

Prefer to wire it up by hand (CI, custom env management, audit)? Add your Vask app credentials to .env:

BROADCAST_CONNECTION=pusher

PUSHER_APP_ID=your_app_key
PUSHER_APP_KEY=your_app_key
PUSHER_APP_SECRET=your_app_secret
PUSHER_HOST=wss.vask.dev
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1

Then follow Laravel's broadcasting docs for the rest of the standard setup.

#vask:doctor

After install (or any time you suspect drift), run the diagnostic:

php artisan vask:doctor
php artisan vask:doctor --no-ping --no-broadcast   # skip live network checks

It validates your PUSHER_* config, optionally pings wss.vask.dev, and optionally fires a test broadcast end-to-end. Source: github.com/vask-dev/laravel.

#Demo route

vask/laravel ships a local-only page at /_vask/demo. Start your dev server, visit it, click an emoji, and watch the round-trip (Laravel → Vask → browser) — including latency — without writing any frontend code. It exercises both server-side broadcasts and Pusher client events.

The route only registers when app()->environment() === 'local'. Disable it entirely with:

VASK_NO_DEMO=true

#Broadcasting Events

#Creating a Broadcast Event

Create an event that implements ShouldBroadcast:

<?php

namespace App\Events;

use App\Models\Message;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MessageSent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public Message $message,
        public string $userName
    ) {}

    public function broadcastOn(): Channel
    {
        return new Channel('chat.' . $this->message->room_id);
    }

    public function broadcastAs(): string
    {
        return 'message.sent';
    }

    public function broadcastWith(): array
    {
        return [
            'id' => $this->message->id,
            'content' => $this->message->content,
            'user' => $this->userName,
            'created_at' => $this->message->created_at->toISOString(),
        ];
    }
}

#Triggering Events

Broadcast events from your controllers or anywhere in your application:

use App\Events\MessageSent;

// In a controller method
public function sendMessage(Request $request)
{
    $message = Message::create([
        'room_id' => $request->room_id,
        'user_id' => auth()->id(),
        'content' => $request->content,
    ]);

    // Broadcast the event
    broadcast(new MessageSent(
        $message,
        auth()->user()->name
    ));

    return response()->json($message);
}

// Or use the event() helper
event(new MessageSent($message, $userName));

// For immediate broadcasting (bypass queue)
broadcast(new MessageSent($message, $userName))->toOthers();

#Client-Side Setup

#Configure Laravel Echo

Initialize Echo in your bootstrap.js or app.{js,ts,tsx} file:

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: 'vask',
    wsHost: 'wss.vask.dev',
    forceTLS: true,
    enabledTransports: ['ws'],
});

#Listening to Events

Subscribe to channels and listen for events in your components:

// Listen to a public channel
Echo.channel('chat.1').listen('.message.sent', (event) => {
    console.log('New message:', event);
    // Update your UI with the new message
    addMessageToChat(event);
});

// Listen to multiple events
Echo.channel('notifications')
    .listen('.user.joined', (e) => {
        console.log(e.userName + ' joined');
    })
    .listen('.user.left', (e) => {
        console.log(e.userName + ' left');
    });

// Leave a channel
Echo.leave('chat.1');

#Private Channels

#Authorization Routes

Define channel authorization logic in routes/channels.php:

<?php

use App\Models\User;
use App\Models\Room;
use Illuminate\Support\Facades\Broadcast;

// Private channel authorization
Broadcast::channel('room.{roomId}', function (User $user, int $roomId) {
    // Return true if user can access this room
    return $user->rooms()->where('room_id', $roomId)->exists();
});

// Return data with authorization
Broadcast::channel('user.{userId}', function (User $user, int $userId) {
    if ($user->id === $userId) {
        return [
            'id' => $user->id,
            'name' => $user->name,
            'avatar' => $user->avatar_url,
        ];
    }
    return false;
});

#Broadcasting to Private Channels

use Illuminate\Broadcasting\PrivateChannel;

class OrderStatusUpdated implements ShouldBroadcast
{
    public function broadcastOn(): Channel
    {
        return new PrivateChannel('user.' . $this->order->user_id);
    }
}

#Listening to Private Channels

// Subscribe to a private channel
Echo.private('user.' + userId)
    .listen('.order.updated', (event) => {
        console.log('Order updated:', event.order);
    })
    .listen('.payment.received', (event) => {
        showNotification('Payment received: $' + event.amount);
    });

#Presence Channels

Presence channels let you track who's online in real-time.

#Authorization with User Data

Broadcast::channel('chat.{roomId}', function (User $user, int $roomId) {
    if ($user->canJoinRoom($roomId)) {
        return [
            'id' => $user->id,
            'name' => $user->name,
            'avatar' => $user->avatar_url,
        ];
    }
});

#Using Presence Channels

use Illuminate\Broadcasting\PresenceChannel;

class UserJoinedRoom implements ShouldBroadcast
{
    public function broadcastOn(): Channel
    {
        return new PresenceChannel('room.' . $this->roomId);
    }
}

#Client-Side Presence

let onlineUsers = [];

Echo.join('room.' + roomId)
    .here((users) => {
        // Initial users in the channel
        console.log('Users online:', users);
        onlineUsers = users;
        updateUsersList(users);
    })
    .joining((user) => {
        // User joined the channel
        console.log(user.name + ' joined');
        onlineUsers.push(user);
        addUserToList(user);
    })
    .leaving((user) => {
        // User left the channel
        console.log(user.name + ' left');
        onlineUsers = onlineUsers.filter((u) => u.id !== user.id);
        removeUserFromList(user);
    })
    .error((error) => {
        console.error('Connection error:', error);
    })
    .listen('.message.sent', (event) => {
        // Listen to events on presence channel
        addMessageToChat(event);
    });

#Webhooks

Vask sends webhooks for channel, presence, and client events. The vask/laravel package gives you typed payloads and auto-registers POST /webhooks/vask the first time it sees a handler — no handler, no route.

Register handlers in a service provider's boot():

use Vask\Laravel\Facades\Vask;
use Vask\Laravel\Webhooks\Payloads\ChannelOccupiedPayload;
use Vask\Laravel\Webhooks\Payloads\ChannelVacatedPayload;
use Vask\Laravel\Webhooks\Payloads\MemberAddedPayload;
use Vask\Laravel\Webhooks\Payloads\MemberRemovedPayload;
use Vask\Laravel\Webhooks\Payloads\ClientEventPayload;

public function boot(): void
{
    Vask::onChannelOccupied(fn (ChannelOccupiedPayload $event) => /* ... */);
    Vask::onChannelVacated(fn (ChannelVacatedPayload $event) => /* ... */);
    Vask::onMemberAdded([MemberHandler::class, 'joined']);
    Vask::onMemberRemoved([MemberHandler::class, 'left']);
    Vask::onClientEvent(LogClientEvent::class); // invokable class
}

The route is registered outside the web middleware group, so CSRF doesn't apply. Customise the path or take over registration yourself:

Vask::webhookPath('/api/vask-hooks');
Vask::disableAutoWebhookRoute();

Full reference: github.com/vask-dev/laravel.

#Troubleshooting

#Run vask:doctor first

php artisan vask:doctor

It catches missing env vars, bad host config, and broken broadcasts before you go hunting through logs.

#Connection Issues

Problem: "Failed to connect to Pusher".

Solution: Check your .env credentials and ensure PUSHER_HOST is set to wss.vask.dev. Re-run php artisan vask:install to rewrite them.

#Events Not Broadcasting

Problem: Events are not being received by clients.

Solutions:

  • Ensure your queue worker is running: php artisan queue:work.
  • Check that BROADCAST_CONNECTION=pusher in .env.
  • Verify the event implements ShouldBroadcast.
  • Check Laravel logs for broadcasting errors.

#Private Channel Authorization Failing

Problem: 403 Forbidden when joining private channels.

Solutions:

  • Verify authorization logic in routes/channels.php.
  • Ensure user is authenticated before subscribing.
  • Check that channel names match between backend and frontend.
  • Verify CSRF token is being sent with authorization requests.

#Debug Mode

Enable Pusher debug mode to see detailed logs:

window.Echo = new Echo({
    // ... other config
    enabledTransports: ['ws', 'wss'],
    // Enable debug mode
    enableLogging: true,
});

// Also enable Pusher logging
Pusher.logToConsole = true;

Prefer raw markdown? View this page as markdown.