WebSocket
Overview
Pogo WebSocket embeds a Reverb/Pusher-compatible WebSocket runtime into FrankenPHP.
It provides:
- a Caddy HTTP handler for WebSocket connections,
- PHP native publish functions,
- a Laravel broadcasting driver,
- standard Pusher private and presence channel signatures from Laravel's broadcasting auth endpoint,
- optional FrankenPHP auth worker fallback when clients do not send standard signatures,
- signed Reverb/Pusher-compatible HTTP publish and management endpoints,
- optional Redis Pub/Sub for multi-node fanout.
Status and fit
Pogo WebSocket is experimental. It is suitable for demos, local testing, and controlled evaluation of a FrankenPHP-native realtime runtime.
Use it when:
- You want to evaluate Pusher-style broadcasting without a separate Node.js or hosted realtime service.
- Your client can use Laravel Echo or the Pusher protocol subset.
- At-most-once realtime delivery is acceptable.
Avoid presenting it as a drop-in production replacement for Laravel Reverb, Pusher, or hosted realtime systems until you validate behavior, benchmarks, and failure modes for your topology.
Supported protocol behavior includes connection establishment, ping/pong, public/private/presence subscriptions, client events on private and presence channels, pusher:signin, signed HTTP event and batch publishing, local channel/user management endpoints, and user connection termination.
Build
Compile the module into FrankenPHP:
xcaddy build \
--with github.com/dunglas/frankenphp@v1.12.4 \
--with github.com/dunglas/frankenphp/caddy@v1.12.4 \
--with github.com/y-l-g/websocket/module@main
Install the Laravel driver:
composer require pogo/websocket
php artisan pogo:ws-install
Caddy configuration
{
frankenphp {
worker {
file public/frankenphp-worker.php
}
}
order pogo_websocket before php_server
}
:8080 {
@websocket path /app/* /apps/* /up /pogo/health
route @websocket {
pogo_websocket {
app_id {$REVERB_APP_ID}
app_key {$REVERB_APP_KEY}
app_secret {$REVERB_APP_SECRET}
# auth_script public/websocket-worker.php
# auth_path /broadcasting/auth
webhook_secret {$POGO_WEBHOOK_SECRET}
allowed_origins https://app.example.com https://admin.example.com
handshake_rate 100
handshake_burst 50
max_connections 10000
max_auth_body 16384
max_concurrent_auth 100
broker_queue_size 1024
shard_queue_size 1024
num_workers 2
num_shards 8
ping_period 54s
pong_wait 60s
write_wait 10s
shutdown_timeout 10s
# redis_host redis:6379
# redis_password {$REDIS_PASSWORD}
# redis_db 0
# redis_tls false
}
}
route {
root * public
encode zstd br gzip
php_server {
index frankenphp-worker.php
try_files {path} frankenphp-worker.php
resolve_root_symlink
}
}
}
By default, WebSocket upgrades accept all origins, matching Reverb's default allowed_origins ['*']. Configure allowed_origins to restrict browser clients. Entries may be *, exact http:// or https:// origins, or host-only values such as app.example.com.
If auth_script is configured and a private or presence subscription omits the standard Pusher auth signature, the module calls the FrankenPHP auth worker and validates the returned signature before joining the channel.
Application integration
Set Laravel environment variables:
BROADCAST_CONNECTION=pogo
REVERB_APP_ID=pogo-app
REVERB_APP_KEY=change-me-to-a-random-public-app-key
REVERB_APP_SECRET=change-me-to-a-long-random-secret
POGO_WEBHOOK_SECRET=change-me-to-a-different-random-secret
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST=localhost
VITE_REVERB_PORT=8080
VITE_REVERB_SCHEME=http
Configure the broadcasting connection:
'pogo' => [
'driver' => 'pogo',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
],
Use Laravel Echo with the Pusher client:
import Echo from 'laravel-echo'
import Pusher from 'pusher-js'
window.Pusher = Pusher
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST || window.location.hostname,
wsPort: import.meta.env.VITE_REVERB_PORT || 80,
wssPort: import.meta.env.VITE_REVERB_PORT || 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME || 'https') === 'https',
disableStats: true,
enabledTransports: ['ws', 'wss'],
})
PHP API
pogo_websocket_publish(string $appId, string $channel, string $event, string $data): int;
pogo_websocket_broadcast_multi(string $appId, string $channels, string $event, string $data): int;
Return status codes:
| Code | Meaning |
|---|---|
0 | Success |
1 | Hub missing |
2 | Channel too long |
3 | Event too long |
4 | Payload too large |
5 | Invalid payload JSON |
6 | Broker publish failed |
7 | Invalid multi-channel JSON |
8 | Broker queue full |
9 | Shard queue full |
The Laravel broadcaster converts native failures into BroadcastException.
Operations
- Use strong, different values for
REVERB_APP_SECRETandPOGO_WEBHOOK_SECRET. - Set
allowed_originsfor browser clients that connect from another origin. - Enforce per-client connection limits at the reverse proxy if FrankenPHP is behind a proxy that hides client IPs.
- Use Redis Pub/Sub only for best-effort multi-node fanout; messages are not persisted, replayed, or acknowledged.
- Prometheus metrics are exposed through Caddy admin metrics at
/metrics. - Use
BROADCAST_CONNECTION=pogofor same-process native publishing. Use a manually configuredreverbconnection only when broadcasts must be sent from another process or container through the HTTP API.
Troubleshooting
- 4100 over capacity: increase
max_connectionsor reduce client count. - 4009 connection unauthorized: verify
app_id,app_key,app_secret, and theREVERB_*values in Laravel and Caddy. - Too many requests: tune
handshake_rateandhandshake_burst. - Private or presence auth fails: confirm Laravel's
/broadcasting/authreturns standard Pusher signatures, or configureauth_scriptfor the FrankenPHP fallback path.