Search by

nsd7 / surge

nsd7

Surge β€” a lightweight, self-hosted, Pusher-compatible realtime broadcasting server for Laravel. No required dependencies.

v1.1.0 2026-10-11 05:43 UTC

README

A lightweight, self-hosted, Pusher-compatible realtime server for Laravel.
Point Laravel Echo at your own server β€” no code changes, no per-message bills.

πŸ“– Documentation Β· Quickstart Β· Laravel Β· How it works Β· Roadmap

Surge speaks the Pusher WebSocket protocol, so it drops straight into an app that already uses Laravel broadcasting and Laravel Echo. Change the host and port in your config and your existing private/presence channels, events, and client events keep working β€” now on infrastructure you own.

The core has zero required dependencies: the WebSocket layer, the Pusher protocol, the HTTP trigger API, and even the Redis scaling client are written against PHP's standard library.

Why

  • Drop-in. Pusher-protocol compatible. Keep Laravel Echo, keep your channels, keep your broadcast() calls. Just repoint them.
  • Self-hosted. No usage caps, no per-message pricing, no third-party seeing your traffic.
  • Light. The default lite runtime is pure PHP β€” one file to run, nothing to compile, runs anywhere PHP runs.
  • Honest. Fails closed on malformed frames, verifies every signature in constant time, and never claims a capability it doesn't have (see Status).

Quickstart

composer require nsd7/surge

Run the server (Pusher-compatible mode is the default):

SURGE_APP_ID=my-app SURGE_APP_KEY=my-key SURGE_APP_SECRET=my-secret \
  vendor/bin/surge serve --port=9019

Connect from the browser with a plain WebSocket, or β€” more usefully β€” with Laravel Echo (below).

Use it with Laravel

Surge is a Laravel package: the service provider auto-registers and adds a surge:serve command. Publish the config if you want to tweak it:

php artisan vendor:publish --tag=surge-config

1. Point the broadcaster at Surge in config/broadcasting.php (the built-in pusher driver β€” no custom driver needed):

'pusher' => [
    'driver' => 'pusher',
    'key'    => env('SURGE_APP_KEY'),
    'secret' => env('SURGE_APP_SECRET'),
    'app_id' => env('SURGE_APP_ID'),
    'options' => [
        'host'   => '127.0.0.1',
        'port'   => 9019,
        'scheme' => 'http',
        'useTLS' => false,
    ],
],

2. Point Echo at Surge in 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_SURGE_APP_KEY,
    wsHost: '127.0.0.1',
    wsPort: 9019,
    forceTLS: false,
    enabledTransports: ['ws'],
});

3. Run the server and broadcast as usual:

php artisan surge:serve
broadcast(new OrderShipped($order)); // delivered over Surge

Private and presence channels authorize through your normal routes/channels.php + /broadcasting/auth endpoint β€” Surge verifies the same HMAC signature Laravel generates.

How it works

Surge is layered so the socket loop, the protocol, and the transport are independent:

          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  HTTP ───▢│  Pusher HTTP API   β”‚   Pusher WS protocol      │◀── WebSocket
  trigger  β”‚  POST /apps/../events  subscribe Β· presence Β·  β”‚     (Laravel Echo)
           β”‚                        client events           β”‚
           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚              β”‚
                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                   β”‚ Runtime (WS) β”‚  β”‚ Scaling (optional) β”‚
                   β”‚ lite: pure   β”‚  β”‚ Redis pub/sub over β”‚
                   β”‚ PHP select   β”‚  β”‚ raw RESP sockets   β”‚
                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • RFC 6455 core β€” buffered frame parser (partial reads, batched frames, 16/64-bit lengths, masking), fragmented-message reassembly, ping/pong, close handling, and send backpressure. Rejects unmasked, oversized, or malformed frames instead of mangling them.
  • Pusher protocol β€” connection_established, public/private/presence channels, pusher:ping/pong, and client-* events.
  • HTTP trigger API β€” POST /apps/{id}/events on the same port, with Pusher REST signature verification matching pusher-php-server.
  • Scaling β€” optional Redis pub/sub so multiple nodes relay events to each other.

Configuration

Everything is env-driven (see config/surge.php):

Variable Default Purpose
SURGE_HOST / SURGE_PORT 0.0.0.0 / 9019 Bind address
SURGE_APP_ID / _KEY / _SECRET β€” App credentials (match broadcasting + Echo)
SURGE_ACTIVITY_TIMEOUT 120 Seconds before a client is asked to ping
SURGE_CLIENT_EVENTS true Allow client-* events on private/presence
SURGE_SCALING_ENABLED false Turn on Redis pub/sub scaling

Testing & benchmarks

The library ships a zero-dependency test runner β€” no PHPUnit needed:

php tests/run.php          # 50 tests
php bench/throughput.php   # local fan-out sanity check

The benchmark reliably fans events out to 100 concurrent clients on a single machine; it's a correctness/sanity harness, not a tuned datacenter benchmark (real throughput depends on your hardware and the test harness is itself single-threaded).

Status

Surge is young. What's real today, and what isn't:

Working & tested

  • RFC 6455 WebSocket server (lite, pure-PHP runtime)
  • Pusher protocol: public/private/presence channels, client events, ping/pong
  • Pusher HTTP trigger API with REST signature verification
  • Laravel package (service provider, config, surge:serve)
  • Redis pub/sub event relay across nodes

Honest caveats

  • The pure-PHP lite runtime is single-process (one core). It is light, not the fastest. A multi-core turbo runtime (OpenSwoole) is included but experimental and not yet load-tested β€” see docs/turbo.md.
  • Cross-node presence is supported but eventually consistent β€” a new node's first roster converges over one relay round-trip (member events still fire). Single-node presence is immediate.
  • There is no TLS termination built in β€” run Surge behind a TLS-terminating proxy (nginx, Caddy) for wss://.

Roadmap

  • Phase 0 β€” Correct RFC 6455 core behind a runtime interface
  • Phase 1 β€” Pusher protocol (channels, presence, client events, auth)
  • Phase 2 β€” Pusher HTTP trigger API (same port)
  • Phase 3 β€” Laravel package (surge:serve, config, Echo compatibility)
  • Phase 4 β€” Redis pub/sub horizontal scaling
  • Phase 5 β€” README, examples, CI, integration + benchmark harness
  • Observability β€” Prometheus /metrics endpoint + structured JSON logging
  • [~] Multi-core turbo runtime (OpenSwoole) β€” code shipped, experimental, pending load-testing & benchmarks (docs/turbo.md)
  • Shared cross-node presence (eventually consistent across nodes)
  • TLS: by design, via a reverse proxy (nginx/Caddy) β€” see the docs

License

MIT Β© nowshad7