Extensions

WebSocket

Embedded Pusher-compatible WebSocket server and Laravel broadcaster.

Overview

Pogo WebSocket embeds a Pusher-compatible WebSocket server into FrankenPHP.

It provides:

  • a Caddy HTTP handler for WebSocket connections,
  • PHP native publish functions,
  • a Laravel broadcasting driver,
  • private and presence channel auth through a dedicated FrankenPHP auth worker,
  • 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, and pusher:signin.

Build

Compile the module into FrankenPHP:

Terminal
xcaddy build \
  --with github.com/dunglas/frankenphp@v1.12.3 \
  --with github.com/dunglas/frankenphp/caddy@v1.12.3 \
  --with github.com/y-l-g/websocket/module@main

Install the Laravel driver:

Terminal
composer require pogo/websocket
php artisan pogo:ws-install

Caddy configuration

Caddyfile
{
  frankenphp {
    worker {
      file public/frankenphp-worker.php
    }
  }

  order pogo_websocket before php_server
}

:8080 {
  route /app/* {
    pogo_websocket {
      app_id pogo-app
      app_secret {$WS_APP_SECRET}
      auth_path /pogo/auth
      auth_script public/websocket-worker.php
      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 requests without an Origin header and browser requests whose Origin host matches the request host. Configure allowed_origins when your frontend connects from a different origin.

Application integration

Set Laravel environment variables:

.env
BROADCAST_CONNECTION=pogo
WS_APP_ID=pogo-app
WS_APP_SECRET=change-me-to-a-long-random-secret
POGO_WEBHOOK_SECRET=change-me-to-a-different-random-secret

VITE_POGO_APP_KEY="${WS_APP_ID}"
VITE_POGO_HOST=localhost
VITE_POGO_PORT=80
VITE_POGO_WSS_PORT=443

Configure the broadcasting connection:

config/broadcasting.php
'pogo' => [
    'driver' => 'pogo',
    'app_id' => env('WS_APP_ID'),
    'secret' => env('WS_APP_SECRET'),
],

Use Laravel Echo with the Pusher client:

resources/js/echo.js
import Echo from 'laravel-echo'
import Pusher from 'pusher-js'

window.Pusher = Pusher

window.Echo = new Echo({
  broadcaster: 'pusher',
  key: import.meta.env.VITE_POGO_APP_KEY || 'pogo-app',
  cluster: 'mt1',
  wsHost: import.meta.env.VITE_POGO_HOST || window.location.hostname,
  wsPort: import.meta.env.VITE_POGO_PORT || 80,
  wssPort: import.meta.env.VITE_POGO_WSS_PORT || 443,
  forceTLS: false,
  disableStats: true,
  enabledTransports: ['ws', 'wss'],
  authEndpoint: '/pogo/auth',
  userAuthentication: {
    endpoint: '/pogo/user-auth'
  }
})

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:

CodeMeaning
0Success
1Hub missing
2Channel too long
3Event too long
4Payload too large
5Invalid payload JSON
6Broker publish failed
7Invalid multi-channel JSON
8Broker queue full
9Shard queue full

The Laravel broadcaster converts native failures into BroadcastException.

Operations

  • Use strong, different values for WS_APP_SECRET and POGO_WEBHOOK_SECRET.
  • Set allowed_origins for 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.

Troubleshooting

  • 4100 over capacity: increase max_connections or reduce client count.
  • 4009 connection unauthorized: verify app_id, app_secret, and WS_APP_SECRET.
  • Too many requests: tune handshake_rate and handshake_burst.
  • Private or presence auth fails: confirm /pogo/auth and /pogo/user-auth are reachable and protected by normal Laravel authentication.