FL

particle-academy/fancy-live

particle-academy/fancy-live

First-party realtime publishing: Pusher-protocol HTTP publish and private/presence channel authorization with zero runtime dependencies, plus an optional Laravel broadcaster. The browser half is fancy-query/socket. Pre-1.0.

Backend · renders no UIPHP
Backend · renders no UI
Nothing to preview — install it and call the typed API, or hand it to an agent.
$composer require particle-academy/fancy-live

API surface

Headless package — drive it from code or an agent. No component grid.

This package renders no UI surface. It exposes a typed PHP API; issues are tracked on GitHub.
readmeREADME.mdView on GitHub →

fancy-live

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.

What next
Install it, then call the API from your code or over MCP.