Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Wave Laravel Package

qruto/laravel-wave

Laravel Wave adds real-time features to Laravel apps without WebSockets. It broadcasts your events to the frontend over Server-Sent Events, with a simple JavaScript client and seamless integration with Laravel's existing event system—no external services required.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish its config:

composer require qruto/laravel-wave
php artisan vendor:publish --tag=wave-config

Open config/wave.php first. That file is the map for the package: the SSE route path, middleware stack, retry/heartbeat behaviour, and whichever storage/driver it uses to persist in-flight events. If the package ships migrations for its event store, run php artisan migrate; if it uses the cache or a Redis stream, confirm the store is shared by every app server.

Wire broadcasting to Wave. Add (or confirm) a wave connection in config/broadcasting.php and point the app at it:

BROADCAST_CONNECTION=wave

Then use ordinary Laravel broadcasting. A first useful case is a per-user private channel for notifications:

class OrderShipped implements ShouldBroadcast
{
    use SerializesModels;

    public function __construct(public Order $order) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('users.'.$this->order->user_id)];
    }
}
broadcast(new OrderShipped($order));

On the front end, open one SSE connection via the package's JS client and subscribe to the channel:

import { Wave } from 'laravel-wave';

const wave = new Wave({ endpoint: '/wave' });

wave.private(`users.${userId}`)
    .listen('OrderShipped', (payload) => console.log(payload));

Check the browser devtools Network tab for the /wave request. You should see Content-Type: text/event-stream, an open connection, and data: frames as events are broadcast.

Implementation Patterns

Treat SSE as a one-way pipe. Server-to-client is the SSE channel; client-to-server is still normal HTTP (POST/PUT), then you broadcast the result. Do not try to build request/response or chat "typing" semantics over the event stream itself.

Keep event classes thin. Broadcast an ID and a small payload (['id' => $order->id, 'status' => 'shipped']) and let the client fetch the full resource over HTTP. Large serialized models bloat every frame and every stored event.

Use private channels by default. Define authorization in routes/channels.php with Broadcast::channel('users.{id}', fn ($user, $id) => (int) $user->id === (int) $id). The JS client authenticates against Laravel's /broadcasting/auth endpoint, so the SSE route and auth route must share the same session/CSRF setup. For multi-tenant apps, put tenant ID in the channel name and re-check membership in the callback.

Centralise the connection. In Vue/React/Svelte, create the Wave instance once (module singleton or provider) and expose a composable/hook that subscribes on mount and calls leave/stopListening on unmount. Multiple EventSource instances per tab waste the browser's small per-origin connection budget.

Broadcast from jobs and model events. Standard ShouldBroadcast events work from HTTP controllers, queued jobs, observers, and php artisan commands. Prefer queued broadcasts (ShouldBroadcast) over ShouldBroadcastNow for user-facing requests so the response is not blocked by storage writes.

Use it alongside, not necessarily instead of, websockets. If you already run Reverb/Pusher for bidirectional features, Wave can cover high-volume, fire-and-forget notification streams while websockets handle presence and client events. Presence and "whisper" style client-to-client messaging are awkward over SSE because there is no persistent client-to-server socket; implement those with HTTP endpoints plus broadcasts.

Testing. Fake the dispatch side with Event::fake([OrderShipped::class]) and assert the event and its channels; test channel authorization callbacks directly with Broadcast::channel resolution or HTTP tests against /broadcasting/auth. Avoid hitting the streaming endpoint in a normal feature test without a timeout — a streaming response can hold the test open. If you must, use a short client timeout and assert headers/status rather than the full stream.

Local development. php artisan serve is single-threaded, so one open SSE connection can block every other request. Use Sail, Valet, Herd, or Octane for day-to-day work, and a separate browser tab/profile when debugging streams.

Gotchas and Tips

  • PHP workers are held for the life of the stream. Each SSE connection occupies a PHP-FPM worker. A handful of users is fine; a few hundred concurrent tabs will exhaust a default pool. Plan for Octane/Swoole, a dedicated pool, or a separate SSE-serving process, and raise max_execution_time, fastcgi_read_timeout, and any upstream idle timeout.
  • Disable proxy buffering. Nginx buffers responses by default; send X-Accel-Buffering: no (the package usually does) and set proxy_buffering off. Also disable gzip for text/event-stream; compression can delay frames indefinitely.
  • Prune the event store. If the package stores events in a table/cache, it grows forever. Schedule the package's prune command (php artisan wave:prune or whatever your version exposes) in routes/console.php/Kernel::schedule, and size the retention window to your longest acceptable reconnect gap.
  • Browser connection limits. HTTP/1.1 caps roughly six connections per origin. Multiplex all channels through the single Wave connection, use HTTP/2 in production, and close the connection on beforeunload/SPA route teardown.
  • Reconnect behaviour. Expect EventSource to reconnect automatically; use the package's retry config plus server-side backoff/jitter to avoid reconnect storms after a deploy. If the stream emits event IDs, honour Last-Event-ID on the server so clients can resume without gaps; if it does not, design for at-least-once and idempotent client handlers.
  • Auth is cookie/CSRF-bound. Private channels fail with 403 when the session cookie is missing (SameSite, cross-domain SPA), the CSRF token is stale, or the SSE route uses different middleware from /broadcasting/auth. Put both behind the same web middleware group.
  • Event names must match. The client listens for the broadcast name, which is the event class basename unless you set broadcastAs(). Namespaced class names, queued events, and broadcastWith() payload shape are the usual "nothing arrives" causes. Log on the server with Log::info() before broadcast() and confirm the frame in the Network tab.
  • Queued broadcasts need a worker. ShouldBroadcast events sit in the queue until queue:work/Horizon is running; in local dev this is the most common reason events never appear. ShouldBroadcastNow bypasses the queue but slows the request.
  • Shared storage across servers. SSE connections are pinned to one app server. If the event store is local (file cache, per-node Redis), events broadcast on node A will never reach a client connected to node B. Use a shared database/Redis and avoid sticky-session-only thinking.
  • Do not dd() or echo debug output mid-stream. Any output before the SSE headers breaks the response. Use logs, and debug with curl -N -H "Accept: text/event-stream" http://app.test/wave to watch raw frames.
  • Config caching. After editing config/wave.php, run php artisan config:clear; a cached config will silently ignore route path, middleware, and retry changes.
  • Extension points. The route middleware stack in config/wave.php is the right place for tenant resolution or extra auth. Channel authorization belongs in routes/channels.php. If you need custom event shaping or storage, wrap broadcasting in your own service rather than forking the package; upgrade cost is low because events remain standard Laravel broadcast events.
  • Version pinning. With an active but small package (last release March 2025), pin the major/minor in composer.json, skim the changelog on upgrade, and keep a fallback path (polling or an existing websocket driver) for critical notification flows.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager