laravel-claim-check maintained by gramercytech
laravel-claim-check
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/authmiddleware. The fetch endpoint defaults to['web'], which only works for classic session-authenticated apps. If yourBroadcastServiceProviderdoesBroadcast::routes(['middleware' => ['web', 'auth:sanctum']])(the usual setup for Inertia / Sanctum / SPA apps), set the same middleware inconfig/claim-check.phpunderroute.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:
- Allow — if non-empty and the event is not in it, pass through unchanged.
- Skip — if the event is in skip, pass through unchanged.
- Always — if the event is in always, claim-check (ignore size).
- 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: 0and 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.123does not let a user fetch it viaprivate-user.456. - Channel authorization. For
private-*andpresence-*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.