PHPackages                             webpatser/resonate-delivery - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. webpatser/resonate-delivery

ActiveLibrary[Queues &amp; Workers](/categories/queues)

webpatser/resonate-delivery
===========================

At-least-once message delivery for Resonate channels: every broadcast logged, replayed to reconnecting subscribers

v0.1.0(2mo ago)00MITPHPPHP ^8.5CI passing

Since May 25Pushed 3w agoCompare

[ Source](https://github.com/webpatser/resonate-delivery)[ Packagist](https://packagist.org/packages/webpatser/resonate-delivery)[ RSS](/packages/webpatser-resonate-delivery/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (8)Versions (2)Used By (0)

Resonate Delivery
=================

[](#resonate-delivery)

At-least-once message delivery for [Resonate](https://github.com/webpatser/resonate) channels. Every broadcast is logged to a per-channel Redis Stream; a reconnecting client sends the id of the last message it saw, and the plugin replays everything since before live delivery resumes.

Solves the single most common WebSocket complaint: "I dropped for 20 seconds and missed messages."

What you get
------------

[](#what-you-get)

- A Laravel broadcaster (`resonate-delivery` driver) that wraps Reverb, logs every broadcast, and tags the payload with a monotonic `_replay_id`.
- A Resonate server plugin that, when a subscribe carries `last_event_id`, replays the missed messages on that channel after `subscription_succeeded` and before any live broadcast.
- Bounded retention via Redis Streams `MAXLEN ~ N`, configurable per-channel.

What this is (and isn't)
------------------------

[](#what-this-is-and-isnt)

- **At-least-once within retention.** With the default cap of 1000 messages per channel, a client that drops for 5 minutes on a low-traffic channel reconnects seamlessly. A client offline for hours on a high-traffic channel sees a clean break — design retention based on expected disconnect duration and your publish rate.
- **Duplicates can happen at the seam.** A message published exactly as a reconnecting client is mid-replay may arrive both in the replay and as a live broadcast. Each message carries a monotonic `_replay_id`; the client deduplicates by it.
- **No ACK protocol.** This is not a message queue. A subscriber that goes away forever does not hold a slot in any per-subscriber outbox. Per-subscriber state lives only on the connection.
- **Server broadcasts only in v0.1.** `client-*` whispers between clients are not logged; they reach connected subscribers only.

Installation
------------

[](#installation)

```
composer require webpatser/resonate-delivery
```

Publish the config to set retention defaults or override the underlying broadcaster:

```
php artisan vendor:publish --tag=resonate-delivery-config
```

Configuring the broadcaster
---------------------------

[](#configuring-the-broadcaster)

Add the wrapping driver to `config/broadcasting.php` and point your default at it:

```
'default' => env('BROADCAST_DRIVER', 'resonate-delivery'),

'connections' => [
    // ... keep your existing 'reverb' connection as-is

    'resonate-delivery' => [
        'driver' => 'resonate-delivery',
        'underlying' => 'reverb',
    ],
],
```

The wrapper delegates auth and the actual WebSocket send to the underlying connection (so your existing Reverb setup is unchanged); it adds the log write and the `_replay_id` augmentation around it.

Registering the server plugin
-----------------------------

[](#registering-the-server-plugin)

In `config/reverb.php`, list the plugin alongside any others you have running:

```
'plugins' => [
    \Webpatser\ResonateDelivery\MessageReplayPlugin::class,
],
```

Restart Resonate (`resonate:reload` for a zero-downtime swap).

Client protocol
---------------

[](#client-protocol)

A client that wants replay sends `last_event_id` in the subscribe payload:

```
{"event": "pusher:subscribe",
 "data": {"channel": "presence-chat.42", "auth": "...", "last_event_id": "1700000000000-0"}}
```

On every message it receives, the client reads `data._replay_id` and remembers the highest. On reconnect, it sends that value. First-time subscribers omit the field and get no replay (normal Pusher behaviour).

For Laravel Echo and the JS Pusher client there is no built-in plug for this; consumers must subscribe with the custom data field, which both libraries support via their lower-level subscription APIs.

Configuration reference
-----------------------

[](#configuration-reference)

KeyDefaultPurpose`connection``REDIS_*` envRedis server hosting the streams. The broadcaster (predis) and the plugin (fledge async) must point at the same server/database.`key_prefix``delivery`Namespace for stream keys (`{prefix}:{appId}:{channel}`).`retention.default_max_messages``1000`Default per-channel cap.`retention.per_channel``[]`fnmatch glob =&gt; max messages overrides.`replay_id_field``_replay_id`The `data` key that carries each message's stream id.`underlying``reverb`Name of the broadcaster the wrapper delegates to.`replay_batch_size``100`XRANGE page size during replay.Requirements
------------

[](#requirements)

- PHP 8.5+
- Laravel 13
- Resonate 0.4+
- Redis 5+ (Redis Streams; included in any modern Redis)

Testing
-------

[](#testing)

```
composer test
```

Tests that touch Redis expect a server on `127.0.0.1:6379` and use database 15; they skip cleanly when no Redis is reachable.

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance92

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity41

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

60d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/e442a1d15a5b64438f3b471acfded80951afb1bed23641cfd80c5254099eab9d?d=identicon)[webpatser](/maintainers/webpatser)

---

Top Contributors

[![webpatser](https://avatars.githubusercontent.com/u/25720?v=4)](https://github.com/webpatser "webpatser (2 commits)")

---

Tags

laravelpusherBroadcastingreplayredis-streamsresonate

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/webpatser-resonate-delivery/health.svg)

```
[![Health](https://phpackages.com/badges/webpatser-resonate-delivery/health.svg)](https://phpackages.com/packages/webpatser-resonate-delivery)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[illuminate/database

The Illuminate Database package.

2.8k54.9M12.2k](/packages/illuminate-database)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M246](/packages/laravel-ai)[moonshine/moonshine

Laravel administration panel

1.3k253.1k86](/packages/moonshine-moonshine)[illuminate/queue

The Illuminate Queue package.

20432.6M1.7k](/packages/illuminate-queue)[harris21/laravel-fuse

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

45955.7k](/packages/harris21-laravel-fuse)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
