gramercytech/laravel-claim-check

Bypass Pusher's 10KB broadcast payload limit via the claim-check pattern. Oversized messages are stored server-side and fetched on demand by clients.

Maintainers

Package info

github.com/GramercyTech/laravel-claim-check

pkg:composer/gramercytech/laravel-claim-check

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-04-30 22:32 UTC

This package is auto-updated.

Last update: 2026-04-30 22:32:29 UTC


README

PHP Tests JS Tests License: MIT

Bypass Pusher's 10KB broadcast payload limit (and similar limits on other broadcasters) using the claim-check pattern: oversized messages are stored server-side in a cache and replaced with a small stub. Clients transparently fetch the full payload on receipt — your application code doesn't change.

Status: v0.1.0 · scaffold complete, integration-tested, not yet published.

Why

Pusher Cloud caps broadcast payloads at 10KB. As applications grow — AI streaming, rich notifications, large change events — that limit is hit sooner than expected, often without warning, and Pusher silently drops the message. Reducing payload size per-event is fine for one or two cases, but doesn't scale.

This package solves the problem at the broadcaster layer:

  • Every outgoing broadcast is measured.
  • If under the threshold (default 8KB), it's sent as-is.
  • If over, the payload is stored in a cache, and a tiny stub is sent in its place: { _cc: 1, u, id, e }.
  • The companion JS package (@gxp-dev/laravel-claim-check) wraps Echo to detect stubs, fetch the full payload, and forward it to your listener.

The pattern is broadcaster-agnostic — it works with Pusher, Ably, Reverb, Soketi, or anything you can wrap.

Install

composer require gramercytech/laravel-claim-check
php artisan vendor:publish --tag=claim-check-config

In your .env:

BROADCAST_DRIVER=claim-check
CLAIM_CHECK_INNER=pusher        # the underlying broadcasting connection to wrap

That's it. The package auto-registers a claim-check connection so you don't need to touch config/broadcasting.php at all — your existing Pusher (or other) connection is wrapped via the inner setting in config/claim-check.php.

⚠️ Match your /broadcasting/auth middleware. The fetch endpoint defaults to ['web'], which only works for classic session-authenticated apps. If your BroadcastServiceProvider does Broadcast::routes(['middleware' => ['web', 'auth:sanctum']]) (the usual setup for Inertia / Sanctum / SPA apps), set the same middleware in config/claim-check.php under route.middleware. Otherwise $request->user() will be null when your channel auth callbacks run and every fetch will 403.

Multiple claim-check connections (optional)

If you want more than one (e.g. a low-threshold connection for chat and a high-threshold one for analytics), explicitly add them to config/broadcasting.php — any setting from config/claim-check.php can be overridden per-connection:

'connections' => [
    'pusher' => [...],

    'pusher-cc-aggressive' => [
        'driver' => 'claim-check',
        'inner' => 'pusher',
        'threshold' => 4096,
        'always' => [\App\Events\AiSessionMessage::class],
    ],
],

Switch via BROADCAST_DRIVER=pusher-cc-aggressive.

Configure

config/claim-check.php:

Key Default Description
inner pusher The underlying broadcasting connection name.
threshold 8192 Bytes; payloads over this are claim-checked. Set to 0 to claim-check every non-empty payload.
ttl 120 Seconds the cached payload is retrievable for.
store null Cache store name (defaults to your default cache).
cache_prefix cc: Cache key prefix.
route.uri claim-check Fetch endpoint URI.
route.middleware ['web'] Middleware applied to the fetch endpoint.
allow_encrypted false Whether to claim-check private-encrypted-* channels.
on_store_failure fall_through fall_through or throw.
allow [] If non-empty, only events in this list are eligible for claim-check.
skip [] Events in this list are never claim-checked, even when oversized.
always [] Events in this list are always claim-checked regardless of size.

Every key can also be overridden per-connection in config/broadcasting.php — see "Multiple claim-check connections" above.

Event filter precedence

Events are matched against whatever string Laravel passes as the broadcast event identifier — by default the FQCN, or the return value of broadcastAs() if your event implements it.

The four lists are applied in this order:

  1. Allow — if non-empty and the event is not in it, pass through unchanged.
  2. Skip — if the event is in skip, pass through unchanged.
  3. Always — if the event is in always, claim-check (ignore size).
  4. Threshold — claim-check when payload size exceeds the threshold.

Implications:

  • Skip overrides always.
  • Always does not bypass the allow filter — if you set an allow list, it's a strict whitelist for everything claim-check ever applies to.
  • Setting threshold: 0 and leaving the lists empty claim-checks every non-empty broadcast on the connection.

How it works

Application                        Broadcaster wrapper                Pusher
    │                                       │                            │
    ├─ event($large)──────────▶  measure ──▶│                            │
    │                                       ├─ store payload in cache    │
    │                                       │  under random 128-bit id   │
    │                                       │                            │
    │                                       └─ send stub {_cc,u,id,e}───▶│
                                                                         │
                                                                  Browser ◀┘
                                                                  ┌────────┐
                                                                  │ Echo   │
                                                                  │ wraps  │
                                                                  │ .listen│
                                                                  └────┬───┘
                                                                       │ POST {id,channel}
   ┌──────────────────────┐                                            ▼
   │ POST /claim-check    │ ◀─ verify channel ∈ original recipients ──┤
   │ controller           │ ◀─ verify user authorized for channel ────┤
   │                      │ ─▶ return original payload ───────────────▶
   └──────────────────────┘                                            │
                                                                       ▼
                                                              listener receives
                                                              full payload

Security

  • Claim IDs are 128-bit random. Unguessable in practice.
  • Channel binding. A claim is only fetchable if the requested channel was a recipient of the original broadcast. Knowing the id of a claim bound to private-admin.123 does not let a user fetch it via private-user.456.
  • Channel authorization. For private-* and presence-* channels, the fetch endpoint re-runs the same channel auth callbacks Laravel uses for /broadcasting/auth. Public channels are not authorized — same as the underlying Pusher delivery.
  • Encrypted channels. Not claim-checked by default; the client-side decryption pipeline does not handle fetched payloads cleanly. Oversized encrypted payloads pass through untouched and will be rejected by Pusher (with a warning logged).

If you need rate-limiting or additional auth on the fetch endpoint, add your own middleware via config('claim-check.route.middleware').

Frontend

See js/README.md for the companion npm package.

Testing

composer install
vendor/bin/pest

Tests use Pest 2/3 + Orchestra Testbench. A spy broadcaster is used as the inner driver so assertions can inspect what would have been sent to Pusher.

Roadmap (post-v1.0)

  • Telescope panel for inspecting claim-checked broadcasts
  • Metrics hooks (claims created, fetched, expired, evicted)
  • Built-in support for re-entering Echo's encryption pipeline on fetched payloads (so encrypted channels can be claim-checked safely)
  • Optional inline encryption of cached payloads at rest

Releasing

Maintainers: see RELEASING.md for the cut-a-release runbook and the one-time Packagist/npm setup.

License

MIT — see LICENSE.md.