PHPackages                             webpatser/resonate-channel-meter - 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. [Payment Processing](/categories/payments)
4. /
5. webpatser/resonate-channel-meter

ActiveLibrary[Payment Processing](/categories/payments)

webpatser/resonate-channel-meter
================================

Record billable and observable channel occupancy periods from Resonate webhooks

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

Since May 25Pushed 3w agoCompare

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

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

Resonate Channel Meter
======================

[](#resonate-channel-meter)

A Laravel package that records billable and observable **channel occupancy periods** from [`webpatser/resonate-webhooks`](https://github.com/webpatser/resonate-webhooks) events. It turns "this room was occupied for 12 minutes" from a polling problem into a Pusher webhook your backend just reacts to.

It is a webhook consumer, not a Resonate plugin. It runs in your Laravel app.

Where it fits
-------------

[](#where-it-fits)

```
Resonate process                Laravel app
────────────────                ───────────
RedisRosterPlugin       webhook
WebhookPlugin     ─────────────►  WebhookController + VerifyPusherSignature
                                      │
                                      ▼
                                  EventHandler
                                      │
                                      ▼
                                  channel_meter_periods table
                                      │
                                      ▼
                                  $chat->totalChannelMeterSeconds(...)

```

The roster makes occupancy cluster-correct. The webhooks plugin pushes the edges out as signed HTTP POSTs. This package receives them and stores them as durable periods you can query and bill on.

How it works
------------

[](#how-it-works)

### One period per occupancy

[](#one-period-per-occupancy)

A `channel_occupied` event opens a `channel_meter_periods` row with `started_at = time_ms` and `ended_at = null`. The matching `channel_vacated` closes it. Both are idempotent: a redelivered webhook never duplicates a period or leaves an orphan close.

### Channel to model resolution

[](#channel-to-model-resolution)

The `channel_meter_periods` table carries a polymorphic `model_type` / `model_id`. A small resolver maps a channel name to the domain entity it represents, using a `{id}` placeholder:

```
// config/resonate-channel-meter.php
'patterns' => [
    'presence-chat.{id}' => \App\Models\Chat::class,
    'presence-call.{id}' => \App\Models\Call::class,
],
```

`presence-chat.42` resolves to `(Chat::class, '42')`, and the period is stored with that link. A channel that matches no pattern is still recorded with the channel name; it just has no model attached. Swap the default `ConfigChannelResolver` for your own by binding `ChannelResolver::class` in a service provider if you need richer rules (tenants, composite keys, lookups).

### Models pull their own periods

[](#models-pull-their-own-periods)

Apply the `HasChannelMeter` trait to any model the resolver maps to:

```
use Webpatser\ResonateChannelMeter\Concerns\HasChannelMeter;

class Chat extends Model
{
    use HasChannelMeter;
}
```

You get:

```
$chat->channelMeterPeriods;            // every period
$chat->openChannelMeterPeriods;        // periods currently in progress
$chat->totalChannelMeterSeconds();     // sum of closed durations
$chat->totalChannelMeterSeconds($from, $to); // clamped to a window
```

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

[](#installation)

```
composer require webpatser/resonate-channel-meter
```

The package migration is auto-loaded. Run it:

```
php artisan migrate
```

Publish the config to register channel patterns:

```
php artisan vendor:publish --tag=resonate-channel-meter-config
```

Registering the webhook endpoint
--------------------------------

[](#registering-the-webhook-endpoint)

The package does not auto-register a route, so you mount the controller wherever you want, behind the signature middleware:

```
use Webpatser\ResonateChannelMeter\Http\Controllers\WebhookController;
use Webpatser\ResonateChannelMeter\Http\Middleware\VerifyPusherSignature;

Route::post('/webhooks/resonate', WebhookController::class)
    ->middleware(VerifyPusherSignature::class);
```

Then point the webhooks plugin at that URL:

```
// config/resonate-webhooks.php
'endpoints' => [
    [
        'url' => env('APP_URL').'/webhooks/resonate',
        'app_id' => '*',
        'events' => ['channel_occupied', 'channel_vacated'],
    ],
],
```

The middleware verifies the `X-Pusher-Signature` against the app secret in `reverb.apps`, so an unsigned or forged request is rejected with `401`. An unknown `X-Pusher-Key` is rejected with `422`.

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

[](#configuration-reference)

KeyDefaultPurpose`patterns``[]`Channel-to-model patterns. Each maps a channel-name shape with a `{id}` placeholder to an Eloquent model class.Notes and caveats
-----------------

[](#notes-and-caveats)

- **Only occupancy events.** This package processes `channel_occupied` and `channel_vacated`; member and client events are ignored. Build a separate consumer if you want to record them.
- **Idempotent.** Receiving the same webhook twice never duplicates a period.
- **`time_ms` comes from the server.** The `started_at` and `ended_at` timestamps are taken from the webhook envelope's `time_ms`, not the receiver's local clock, so a queued retry still records the original moment.
- **Open periods are excluded from totals.** `totalChannelMeterSeconds()` ignores any period whose `ended_at` is still null. Close stale periods explicitly if you need to bill an in-progress session.

Requirements
------------

[](#requirements)

- PHP 8.5+
- Laravel 13
- `webpatser/resonate-webhooks` running against a Resonate server

Testing
-------

[](#testing)

```
composer test
```

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 (3 commits)")

---

Tags

laravelbillingpusherwebhooksmeteringresonate

###  Code Quality

TestsPest

### Embed Badge

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

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

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k30.2M151](/packages/laravel-cashier)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M136](/packages/laravel-pulse)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

77922.3M186](/packages/laravel-mcp)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9762.4M133](/packages/roots-acorn)

PHPackages © 2026

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