Search by

particle-academy / fancy-live

wishborn

Framework-independent Pusher HTTP publishing and channel authorization, with an optional Laravel broadcaster.

Package info

github.com/Particle-Academy/fancy-live

pkg:composer/particle-academy/fancy-live

Statistics

Installs: 78

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-10-24 02:37 UTC

This package is auto-updated.

Last update: 2026-10-27 12:52:07 UTC


README

First-party Pusher HTTP publishing and protected-channel authorization for PHP. The core requires PHP 8.4+ and no framework or third-party runtime dependency. An optional Laravel 13 broadcaster uses the same publisher and signer.

composer require particle-academy/fancy-live

Publish an event

use FancyLive\Config;
use FancyLive\Publisher;

$config = new Config(
    endpoint: 'https://socket.example', // HTTP API root, not the WebSocket URL
    appId: 'your-app-id',
    key: 'your-public-app-key',
    secret: 'your-server-only-secret',
);
(new Publisher($config))->publish(['private-room.7'], 'cms.updated', ['revision' => 'r1']);

The native transport sends once, verifies TLS, refuses redirects and uses a three-second timeout (configurable up to 30 seconds). HTTP failures throw a LiveFailure with a fixed reason and optional httpStatus, without exposing signed URLs or response bodies. Hosts can supply their own Transport; it must preserve the exact SignedRequest bytes and avoid automatic retries/redirects. The native transport needs PHP HTTP streams enabled and a configured CA trust store for HTTPS. An explicit HTTP endpoint supports a trusted local server.

Publishing accepts 1–100 distinct channels, each up to 164 ASCII characters, and a JSON payload encoded to at most 10,000 bytes. Objects remain objects; invalid UTF-8, nonfinite values and excessive depth fail before transport. Reserved protocol events and encrypted channels are refused. An optional fourth argument excludes a sender socket ID. Large state belongs in your database: broadcast a revision or invalidation, then let clients read authoritative state.

Authorization

The host must authenticate the caller and authorize the specific channel before calling these signing methods:

$authorization = new FancyLive\Authorization($config);
$private = $authorization->privateChannel('1234.1234', 'private-room.7');
$presence = $authorization->presenceChannel(
    '1234.1234', 'presence-room.7', $authenticatedUserId, ['name' => $displayName],
);

Return the result as JSON. Presence channel_data is the exact JSON string included in its signature. Never send the app secret to the browser. There are no built-in HTTP routes or allow-all authorization policy.

Laravel

The service provider is auto-discovered when installed in Laravel. Configure server-only environment variables:

BROADCAST_CONNECTION=fancy-live
FANCY_LIVE_HTTP_URL=https://socket.example
FANCY_LIVE_APP_ID=your-app-id
FANCY_LIVE_APP_KEY=your-public-app-key
FANCY_LIVE_APP_SECRET=your-server-only-secret

The provider registers a fancy-live driver and a default connection when none is configured. Existing connection options override package defaults. It adds no routes. Register Laravel broadcasting routes with your authenticated web middleware and your normal Broadcast::channel policies. Private/presence auth requires a resolved user and a successful host policy; presence requires an array of host-approved metadata. The authenticated user's broadcasting ID is used. No JSONP callback is accepted.

For example, register a per-user policy in the host's service provider after configuring the connection:

use Illuminate\Support\Facades\Broadcast;

Broadcast::connection('fancy-live')->channel('user.{id}',
    static fn ($user, string $id): bool => (string) $user->getAuthIdentifier() === $id,
);

Put the endpoint in the host's routes/web.php, which already applies the web session and request-forgery middleware. Choose the application's authentication guard and rate limit; do not exempt this route from those protections:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Route;

Route::post('/broadcasting/auth', static function (Request $request) {
    return response()->json(Broadcast::connection('fancy-live')->auth($request));
})->middleware(['auth', 'throttle:60,1']);

The client sends socket_id and channel_name (for example private-user.7). The broadcaster checks the resolved user and matching host policy before signing. For a presence channel, return an approved metadata array from its policy instead of true; the broadcaster obtains the identity from the authenticated user. If the host already registers Laravel's broadcasting auth route, use that route and its configured middleware instead of registering a duplicate endpoint.

Laravel Channel, PrivateChannel, PresenceChannel, custom event names and sender exclusion use the same core publishing path. A broadcast is limited to 100 channels; it does not silently split a publish into partial batches. Events should be dispatched after the database transaction commits.

Pair with fancy-query/socket

The browser transport ships in @particle-academy/fancy-query 0.9.0 and later. It holds only the public app key. The HTTP API endpoint and app secret remain on the server; the WebSocket URL uses the managed service's browser endpoint.

npm install @particle-academy/fancy-query@^0.9.0
import { FancyDataRoot } from "@particle-academy/fancy-query";
import { createSocketEcho } from "@particle-academy/fancy-query/socket";

function liveClient(csrfToken: string) {
  return createSocketEcho({
    url: "wss://socket.example/app/PUBLIC_APP_KEY",
    authorize: async ({ socketId, channelName }, signal) => {
      const response = await fetch("/broadcasting/auth", {
        method: "POST", credentials: "same-origin", signal,
        headers: { "Content-Type": "application/json", Accept: "application/json", "X-CSRF-TOKEN": csrfToken },
        body: JSON.stringify({ socket_id: socketId, channel_name: channelName }),
      });
      if (!response.ok) throw new Error("Channel authorization refused.");
      return response.json();
    },
  });
}

// Create one client per browser app using the host-provided CSRF token.
// <FancyDataRoot echo={echo}>{children}</FancyDataRoot>

For a literal event name such as cms.updated, listen with a leading dot (.cms.updated) or disable the client's Laravel event namespace. Use Fancy query to invalidate/refetch authoritative state when an event arrives and after every subscription acknowledgement, including reconnection. The transport has no client publishing or encrypted-channel support. The host owns the WebSocket service, credentials, channel policy and reconnect reconciliation.

Tests and protocol references

composer install
composer validate --strict
composer test
composer run test:core

Development uses the owner-approved Pest 5 framework and Laravel 13 for the optional integration tests. These are development dependencies only; consumers of the core still require only PHP. composer test runs vendor/bin/pest with this repository's Composer autoloader. composer run test:core starts a fresh PHP process with no Composer or framework loader, also a separate CI step.

The unit tests inspect exact transport arguments without opening a server. Laravel integration tests use the real framework broadcaster and driver manager. A real managed socket server/browser round trip is a release gate.

The HTTP and private-channel vectors come from the official HTTP API and authorization references. For the presence example's exact stated bytes, PHP and Python independently produce 31935e7d86dba64c2a90aed31fdc61869f9b22ba9d8863bba239c03ca481bc80; the digest printed in that documentation differs. The test preserves the stated bytes and independently verified digest.