PHPackages                             triyatna/broadcasty-lary - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. triyatna/broadcasty-lary

ActiveLibrary

triyatna/broadcasty-lary
========================

Feature-rich, secure, multi-tenant realtime broadcasting for Laravel 12 (WS/SSE/Push/Replay/RBAC).

020PHP

Since Oct 6Pushed 7mo agoCompare

[ Source](https://github.com/triyatna/broadcasty-lary)[ Packagist](https://packagist.org/packages/triyatna/broadcasty-lary)[ RSS](/packages/triyatna-broadcasty-lary/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependenciesVersions (1)Used By (0)

Broadcasty-Lary
===============

[](#broadcasty-lary)

[![PHP](https://camo.githubusercontent.com/fea263d3c3d39ecff0644ff2354e08d83e01461c667f3525892d8ca31739bcd0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e322d3737374242343f6c6f676f3d706870)](https://camo.githubusercontent.com/fea263d3c3d39ecff0644ff2354e08d83e01461c667f3525892d8ca31739bcd0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e322d3737374242343f6c6f676f3d706870)[![Laravel](https://camo.githubusercontent.com/3d9c6b5ae9bf4a266a0111ab622c86271130f96e4fd3062a0a44d8ad66cae46c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d31322e782d4646324432303f6c6f676f3d6c61726176656c)](https://camo.githubusercontent.com/3d9c6b5ae9bf4a266a0111ab622c86271130f96e4fd3062a0a44d8ad66cae46c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d31322e782d4646324432303f6c6f676f3d6c61726176656c)[![License](https://camo.githubusercontent.com/8bb50fd2278f18fc326bf71f6e88ca8f884f72f179d3e555e20ed30157190d0d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e2e737667)](https://camo.githubusercontent.com/8bb50fd2278f18fc326bf71f6e88ca8f884f72f179d3e555e20ed30157190d0d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e2e737667)

 [ ![Broadcasty-Lary](https://raw.githubusercontent.com/triyatna/triyatna.github.io/refs/heads/main/assets/broadcast-lary.png) ](https://github.com/triyatna/broadcasty-lary)

Feature-rich, secure, multi-tenant realtime broadcasting for **Laravel 12**.
Transports: **Auto** (Reverb/Pusher/Ably/Redis), **SSE** endpoint included.
Use cases: realtime data, notifications, presence, replay, RBAC, and push notifications (Web Push, FCM, APNs, OneSignal).
Frontend: framework-agnostic **TypeScript SDK** with auto WS/SSE, reconnect, backpressure, offline queue, and request/response.

- Minimal comments, English code
- Defaults safe and lightweight
- Zero-config Tailwind friendly (UI you build)

---

Table of Contents
-----------------

[](#table-of-contents)

1. [Features](#features)
2. [Why Broadcasty-Lary?](#why-broadcasty-lary)
3. [Requirements](#requirements)
4. [Architecture Overview](#architecture-overview)
5. [Installation](#installation)
6. [Quick Start](#quick-start)
7. [Configuration](#configuration)
8. [.env Reference](#env-reference)
9. [Environment Variables – Complete Guide](#environment-variables--complete-guide)
10. [Laravel Integration](#laravel-integration)
11. [JS SDK (TypeScript)](#js-sdk-typescript)
12. [Realtime: Publish/Subscribe](#realtime-publishsubscribe)
13. [Presence](#presence)
14. [RBAC Channel Policy](#rbac-channel-policy)
15. [Replay API](#replay-api)
16. [Push Notifications](#push-notifications)
17. [Automatic Drivers &amp; Failover](#automatic-drivers--failover)
18. [Database &amp; Storage Model](#database--storage-model)
19. [Observability &amp; Admin](#observability--admin)
20. [Security Hardening](#security-hardening)
21. [Multi-Tenant Model](#multi-tenant-model)
22. [Request/Response Pattern](#requestresponse-pattern)
23. [Offline Queue &amp; Backpressure](#offline-queue--backpressure)
24. [Rate Limiting &amp; Throttling](#rate-limiting--throttling)
25. [Disaster Readiness](#disaster-readiness)
26. [Testing &amp; Benchmarks](#testing--benchmarks)
27. [Troubleshooting](#troubleshooting)
28. [External Provider Setup &amp; Docs](#external-provider-setup--docs)
29. [FAQ](#faq)
30. [Contributors](#contributors)
31. [License](#license)

---

Features
--------

[](#features)

- **Transports**
    - Auto driver selection: **Reverb → Pusher → Ably → Redis** (configurable)
    - **SSE** streaming endpoint (works under PHP-FPM/Octane)
- **Realtime Messaging**
    - Ordered events per **partition** with idempotency
    - **Replay API**: read from sequence offsets with retention policies
    - **Presence** with separate keyspace and TTL
- **Security**
    - **JWT** verification with required claims (`sub`, `tid`, `roles`, `iat`, `exp`)
    - Nonce/timestamp anti-replay, payload caps, CORS allowlist, TLS stance
- **RBAC Channel Policy**
    - Pluggable resolver, defaults for `private-*`, `presence-*`, publish roles
    - Wildcard/topic ready
- **Push Notifications**
    - **Web Push (VAPID)**, **FCM**, **APNs**, **OneSignal**
- **Observability**
    - **Prometheus** metrics endpoint, **OpenTelemetry** hooks
    - Audit log table, structured logging
- **Admin Toolkit Hooks**
    - Channel explorer, presence viewer, replay debugger, audit viewer (UI up to you)
- **SDK (TypeScript)**
    - Auto **WS/SSE**, reconnect with jitter, **backpressure** modes, **offline queue**
    - Request/Response (correlationId + timeouts)
- **Multi-Tenant First-Class**
    - Tenant isolation prefixes, required token claim
- **Disaster-Ready**
    - Auto failover across drivers, Redis replay fallback, rotation commands

---

Why Broadcasty-Lary?
--------------------

[](#why-broadcasty-lary)

- **Drop-in** for Laravel 12 with safe defaults
- **Reusable** across Blade, Inertia (Vue/React), Livewire
- **Secure**: JWT + replay protection + RBAC + caps + CORS allowlist
- **Operationally sane**: metrics, tracing hooks, audit logs
- **No complex infra required**: SSE path runs with PHP-FPM; WS via Reverb/Pusher/Ably if desired

---

Requirements
------------

[](#requirements)

- PHP **8.2+**, Laravel **12**
- Redis (recommended; presence/replay rely on it even if using Ably/Pusher/Reverb)
- OpenSSL (TLS/JWT)
- Node **18+** to build the JS SDK (optional if consuming prebuilt)

---

Architecture Overview
---------------------

[](#architecture-overview)

- **Package** provides:
    - Routes: `/broadcasty/sse`, `/broadcasty/publish`, `/broadcasty/replay`, `/broadcasty/presence/*`
    - Drivers: Redis, Pusher-compatible (incl. Reverb), Ably, Null
    - Security middlewares and policy resolver
    - Replay store and presence store
    - Push bridges (Web Push/FCM/APNs/OneSignal)
    - Console: installer, key rotation, serve info
- **Client SDK** abstracts networking and resilience (reconnect, backpressure, offline)

---

Installation
------------

[](#installation)

```
composer require triyatna/broadcasty-lary
php artisan vendor:publish --tag=broadcasty-config
php artisan migrate
php artisan broadcasty:install
```

The installer:

- Publishes config
- Runs migrations
- Ensures `.env` exists and appends essential keys (idempotent; `--force` to overwrite appended keys)

Quick Start
-----------

[](#quick-start)

Issue a JWT on login, then use the JS SDK in any frontend.

See `resources/js-sdk` for a framework-agnostic TypeScript SDK with auto WS/SSE, backpressure, reconnect, offline queue.

---

Configuration
-------------

[](#configuration)

File: `config/broadcasty.php`

Key areas:

- `default_driver`: `auto | redis | ably | pusher | reverb | null`
- `auto.order`: resolution order for auto mode
- `handshake`: JWT public keys, allowed algs, skew, required claims, nonce TTL
- `rbac.resolver`: policy resolver class
- `replay`: store (`redis|database`), retention, partitions
- `presence`: TTL, separate key prefix
- `push`: webpush/fcm/apns/onesignal toggles and credentials
- `limits`: payload caps and rates
- `security`: TLS stance, HMAC, timestamp tolerance, origins
- `observability`: Prometheus path, OpenTelemetry flag
- `tenancy`: required header/claim names, prefixing

---

.env Reference
--------------

[](#env-reference)

Minimal:

```
BROADCASTY_DRIVER=auto
BROADCASTY_REDIS_CONN=default
BROADCASTY_REDIS_PREFIX=bcy:
BROADCASTY_AUTO_ORDER=reverb,pusher,ably,redis
BROADCASTY_ALLOWED_ORIGINS=*
BROADCASTY_PROM=true
BROADCASTY_JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nREPLACE_ME\n-----END PUBLIC KEY-----"
```

Optional providers:

```
# Reverb (Pusher protocol)
REVERB_APP_ID=...
REVERB_APP_KEY=...
REVERB_APP_SECRET=...
REVERB_HOST=reverb.yourapp.tld
REVERB_PORT=443
REVERB_SCHEME=https

# Pusher
PUSHER_APP_ID=...
PUSHER_APP_KEY=...
PUSHER_APP_SECRET=...
PUSHER_HOST=api-pusher.pusher.com
PUSHER_PORT=443
PUSHER_SCHEME=https

# Ably
ABLY_KEY=key:secret
```

Push bridges:

```
# Web Push
BROADCASTY_WEBPUSH=true
WEBPUSH_VAPID_SUBJECT=mailto:you@example.com
WEBPUSH_VAPID_PUBLIC_KEY=...
WEBPUSH_VAPID_PRIVATE_KEY=...

# FCM
BROADCASTY_FCM=false
FCM_SERVER_KEY=...

# APNs (provider token JWT required; wire in ApnsBridge)
BROADCASTY_APNS=false
APNS_KEY_ID=...
APNS_TEAM_ID=...
APNS_P8_PATH=/secure/AuthKey_XXXX.p8
APNS_BUNDLE_ID=com.your.app
APNS_SANDBOX=false

# OneSignal
BROADCASTY_ONESIGNAL=false
ONESIGNAL_APP_ID=...
ONESIGNAL_API_KEY=...
```

---

Environment Variables – Complete Guide
--------------------------------------

[](#environment-variables--complete-guide)

Set these in your project’s `.env`. Each variable below explains **what it is**, **when you need it**, and **how to obtain or format it**. If a provider is unused, leave its vars empty.

### Core / General

[](#core--general)

KeyRequiredWhat it doesHow to set / Notes`BROADCASTY_DRIVER`YesSelects transport driver. Use `auto` for detection and failover.`auto` | `redis` | `ably` | `pusher` | `reverb` | `null`. Default: `auto`.`BROADCASTY_AUTO_ORDER`NoDriver preference order in `auto` mode.Comma list; default: `reverb,pusher,ably,redis`.`BROADCASTY_ALLOWED_ORIGINS`RecommendedCORS allowlist for browsers.Use exact origins: e.g. `https://app.example.com,https://admin.example.com`. Use `*` only for local/dev.`BROADCASTY_PROM`NoEnables Prometheus metrics route.`true` or `false`. Default `true`.`BROADCASTY_JWT_PUBLIC_KEY`YesPublic key used to verify client JWTs.Paste PEM **public** key (multiline). See “JWT Keys” below.`BROADCASTY_REDIS_CONN`RecommendedLaravel Redis connection name.Matches `config/database.php` `redis.connections`. Usually `default`.`BROADCASTY_REDIS_PREFIX`NoPrefix for Broadcasty keys.Default `bcy:`. Change if you share Redis across apps.#### JWT Keys (how to generate)

[](#jwt-keys-how-to-generate)

```
# RSA 2048 example
openssl genrsa -out jwtRS256.key 2048
openssl rsa -in jwtRS256.key -pubout -out jwtRS256.key.pub
# Put the *public* key content (jwtRS256.key.pub) into BROADCASTY_JWT_PUBLIC_KEY
# Keep the private key in your app/IdP to issue client tokens.
```

Paste public key as a single line in `.env` with `\n` newlines:

```
BROADCASTY_JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh...snip...\n-----END PUBLIC KEY-----"

```

---

### Redis (baseline for presence/replay even if you use a provider)

[](#redis-baseline-for-presencereplay-even-if-you-use-a-provider)

KeyRequiredNotes`BROADCASTY_REDIS_CONN=default`RecommendedEnsure `redis` is configured in `config/database.php` and Redis server is reachable.`BROADCASTY_REDIS_PREFIX=bcy:`RecommendedNamespace isolation for keys.> Redis install: Ubuntu `apt install redis-server`; Docker `docker run -p 6379:6379 redis`. Laravel config:

---

### Laravel Reverb (Pusher protocol)

[](#laravel-reverb-pusher-protocol)

Use **Reverb** if you want a first-party WebSocket service for Laravel.
Docs:

KeyRequiredWhat`REVERB_APP_ID`Yes (if using Reverb)Your Reverb app ID.`REVERB_APP_KEY`YesPublic key.`REVERB_APP_SECRET`YesSecret key.`REVERB_HOST`YesHost of your Reverb server, e.g. `reverb.example.com`.`REVERB_PORT`NoUsually `443`.`REVERB_SCHEME`No`https` for TLS (recommended).> Set `BROADCASTY_DRIVER=auto` and fill Reverb vars; Broadcasty will pick Reverb first by default.

---

### Pusher

[](#pusher)

Create an app and obtain keys at:

KeyRequiredWhat`PUSHER_APP_ID`Yes (if using Pusher)App ID.`PUSHER_APP_KEY`YesPublic key.`PUSHER_APP_SECRET`YesSecret key.`PUSHER_HOST`Yes`api-pusher.pusher.com` or your cluster host.`PUSHER_PORT`No`443` (TLS).`PUSHER_SCHEME`No`https`.> In `auto` mode, Pusher is attempted if Reverb is absent/unhealthy and Pusher vars are present.

---

### Ably

[](#ably)

Create an API key at:

KeyRequiredWhat`ABLY_KEY`Yes (if using Ably)In the form `appKey:secret`.> In auto mode, Ably is tried after Reverb/Pusher when `ABLY_KEY` is present.

---

### Web Push (VAPID) – Browser Notifications

[](#web-push-vapid--browser-notifications)

Generate VAPID keys using your preferred tool (e.g. PHP WebPush or node-web-push).

- PHP:
- Node:

KeyRequiredWhat`BROADCASTY_WEBPUSH`No`true` to enable Web Push bridge.`WEBPUSH_VAPID_SUBJECT`Yes when enabledContact URI `mailto:you@example.com``WEBPUSH_VAPID_PUBLIC_KEY`Yes when enabledBase64URL public key.`WEBPUSH_VAPID_PRIVATE_KEY`Yes when enabledPrivate key.> Client must register a service worker and call `pushManager.subscribe()` using your `PUBLIC_KEY` (base64url).

---

### Firebase Cloud Messaging (FCM)

[](#firebase-cloud-messaging-fcm)

Create a Firebase project → Cloud Messaging → Server key.
Console:

KeyRequiredWhat`BROADCASTY_FCM`No`true` to enable FCM bridge.`FCM_SERVER_KEY`Yes when enabledLegacy server key (HTTP v1 not required for this bridge).> Client must provide its device/web FCM token to your backend; send with `['token' => $fcmToken]`.

---

### Apple Push Notification service (APNs)

[](#apple-push-notification-service-apns)

Set up a Key (.p8) in Apple Developer → Keys → APNs.
Docs: [https://developer.apple.com/documentation/usernotifications/setting\_up\_a\_remote\_notification\_server](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server)

KeyRequiredWhat`BROADCASTY_APNS`No`true` to enable APNs bridge.`APNS_KEY_ID`Yes when enabledKey ID of your .p8.`APNS_TEAM_ID`Yes when enabledApple Team ID.`APNS_P8_PATH`Yes when enabledAbsolute path to `.p8` file (secure location).`APNS_BUNDLE_ID`Yes when enablediOS bundle identifier.`APNS_SANDBOX`No`true` for sandbox; `false` for production.> You must generate an APNs provider **JWT** for each request. Hook your JWT creation inside `ApnsBridge` where indicated.

---

### OneSignal

[](#onesignal)

Create an app and find App ID + REST API Key at:

KeyRequiredWhat`BROADCASTY_ONESIGNAL`No`true` to enable OneSignal bridge.`ONESIGNAL_APP_ID`Yes when enabledApp ID.`ONESIGNAL_API_KEY`Yes when enabledREST API Key.> Client must provide `player_id` (OneSignal device ID). Send payload with `['player_id' => '...']`.

---

### CORS, TLS, and Origins

[](#cors-tls-and-origins)

- Always serve production over **HTTPS**.
- Set `BROADCASTY_ALLOWED_ORIGINS` to comma-separated origins, e.g. `https://app.example.com,https://admin.example.com`.
- Avoid `*` in production.

---

### Provider Selection Cheatsheet

[](#provider-selection-cheatsheet)

- Use **Reverb** for first-party WS in Laravel. Fill `REVERB_*` and keep `BROADCASTY_DRIVER=auto`.
- Use **Pusher** if you prefer managed WS; fill `PUSHER_*`.
- Use **Ably** alternatively; fill `ABLY_KEY`.
- Regardless of provider, keep **Redis** available for presence/replay.

---

Laravel Integration
-------------------

[](#laravel-integration)

### Issue JWT for the client

[](#issue-jwt-for-the-client)

```
// routes/web.php
use Triyatna\Broadcasty\Security\Jwt;

Route::post('/auth/bcy-token', function () {
    $u = request()->user();
    $jwt = app(Jwt::class)->issue([
        'sub'   => (string)$u->id,
        'tid'   => (string)($u->tenant_id ?? 'default'),
        'roles' => $u->roles()->pluck('name')->all(),
        'jti'   => (string) \Str::uuid(),
    ], 900);
    return ['token' => $jwt];
})->middleware('auth');
```

Store this token in your SPA (e.g., `localStorage.setItem('bcy_jwt', token)`).

---

JS SDK (TypeScript)
-------------------

[](#js-sdk-typescript)

Folder: `resources/js-sdk/`

Build:

```
cd resources/js-sdk
npm i
npm run build
# Optional: npm publish --access public
```

Consume:

```
import BroadcastyClient from "@triyatna/broadcasty-js";
// or from copied source: '@/vendor/broadcasty/index';

export const bcy = new BroadcastyClient({
  baseUrl: import.meta.env.VITE_APP_URL,
  getToken: () => localStorage.getItem("bcy_jwt")!,
  transport: "auto",
  backpressure: "queue",
});
```

---

Realtime: Publish/Subscribe
---------------------------

[](#realtime-publishsubscribe)

### Subscribe (with replay and reconnect)

[](#subscribe-with-replay-and-reconnect)

```
const sub = await bcy.subscribe({
  channel: "private-notify:user:42",
  fromSequence: 0,
  onMessage: (raw) => {
    const msg = typeof raw === "string" ? JSON.parse(raw) : raw;
    // update UI
  },
});
// later: sub.close()
```

### Publish (ordered + idempotent)

[](#publish-ordered--idempotent)

```
await bcy.publish({
  channel: "private-notify:user:42",
  payload: { type: "status", orderId: 123, state: "PAID" },
  meta: { schema: "order.v1" },
  timeoutMs: 8000,
});
```

Server endpoint: `POST /broadcasty/publish`
SSE stream: `GET /broadcasty/sse?channel=...`
Replay: `GET /broadcasty/replay?channel=...&partition=0&from=0`

---

Presence
--------

[](#presence)

Join/Leave:

```
# Join
curl -X POST /broadcasty/presence/join \
 -H "Authorization: Bearer " -H "Content-Type: application/json" \
 -d '{"channel":"presence-room:global","member":{"id":"42","name":"TY"}}'

# Leave
curl -X POST /broadcasty/presence/leave \
 -H "Authorization: Bearer " -H "Content-Type: application/json" \
 -d '{"channel":"presence-room:global","id":"42"}'
```

Presence keys are kept separate with TTL, avoiding interference with replay keys.

---

RBAC Channel Policy
-------------------

[](#rbac-channel-policy)

Default rules:

- `private-*`, `presence-*`: require `member` role to subscribe
- Publishing requires `admin` or `publisher` role
- Public channels are open to subscribe

Swap resolver:

```
// config/broadcasty.php
'rbac' => [
  'resolver' => \App\Broadcast\MyPolicyResolver::class,
],
```

Implement:

```
use Triyatna\Broadcasty\Contracts\PolicyResolver;

class MyPolicyResolver implements PolicyResolver {
  public function authorize(string $tenant, string $channel, string $userId, array $roles, string $action): bool {
    // your rules
    return true;
  }
  public function partitions(string $tenant, string $channel): int {
    return str_starts_with($channel,'highload-') ? 8 : 1;
  }
}
```

---

Replay API
----------

[](#replay-api)

Catch up from sequence offsets:

```
GET /broadcasty/replay?channel=private-notify:user:42&partition=0&from=0
Authorization: Bearer

```

Configure retention/partitioning in `config/broadcasty.php`.

---

Push Notifications
------------------

[](#push-notifications)

### Web Push (browser)

[](#web-push-browser)

Service worker (`public/sw.js`):

```
self.addEventListener("push", (e) => {
  const data = e.data?.json() || {};
  e.waitUntil(
    self.registration.showNotification(data.title || "Notification", {
      body: data.body || "",
      icon: "/icon.png",
      data,
    })
  );
});
self.addEventListener("notificationclick", (e) => {
  e.notification.close();
  const url = e.notification.data?.url || "/";
  e.waitUntil(clients.openWindow(url));
});
```

Subscribe from SPA:

```
async function enableWebPush() {
  const reg = await navigator.serviceWorker.register("/sw.js");
  const sub = await reg.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: "",
  });
  await fetch("/push/subscribe", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-TOKEN": (window as any).csrf,
    },
    body: JSON.stringify(sub),
  });
}
```

Store subscription &amp; send:

```
use Triyatna\Broadcasty\Push\WebPushBridge;

Route::post('/push/subscribe', function () {
    request()->validate(['endpoint'=>'required','keys'=>'required|array']);
    \DB::table('user_push_subs')->updateOrInsert(
        ['user_id'=>auth()->id(), 'tenant_id'=>auth()->user()->tenant_id, 'endpoint'=>request('endpoint')],
        ['payload'=>json_encode(request()->only('endpoint','keys'))]
    );
    return ['ok'=>true];
})->middleware('auth');

Route::post('/push/send', function () {
    $subs = \DB::table('user_push_subs')->where('user_id', request('user_id'))->get();
    $bridge = app(\Triyatna\Broadcasty\Push\WebPushBridge::class);
    foreach ($subs as $s) {
        $bridge->send(json_decode($s->payload, true), [
            'title' => 'Update',
            'body'  => 'Order #123 is PAID',
            'url'   => url('/orders/123')
        ]);
    }
    return ['ok'=>true];
})->middleware('auth');
```

### FCM (Android/web), APNs (iOS), OneSignal

[](#fcm-androidweb-apns-ios-onesignal)

```
// FCM
app(\Triyatna\Broadcasty\Push\FcmBridge::class)->send(
  ['token' => $fcmToken],
  ['title'=>'Hello','body'=>'FCM works','data'=>['foo'=>'bar']]
);

// APNs (implement Apple provider JWT and set headers inside bridge)
app(\Triyatna\Broadcasty\Push\ApnsBridge::class)->send(
  ['deviceToken' => $apnsDeviceToken],
  ['title'=>'Hello','body'=>'APNs works','url'=>url('/')]
);

// OneSignal
app(\Triyatna\Broadcasty\Push\OneSignalBridge::class)->send(
  ['player_id' => $playerId],
  ['title'=>'Hello','body'=>'OneSignal works','url'=>url('/')]
);
```

---

Automatic Drivers &amp; Failover
--------------------------------

[](#automatic-drivers--failover)

- `BROADCASTY_DRIVER=auto`
- Order: `BROADCASTY_AUTO_ORDER=reverb,pusher,ably,redis`
- Health probes with a short circuit breaker choose the first healthy driver
- Change driver by editing `.env`, no code changes

---

---

Database &amp; Storage Model
----------------------------

[](#database--storage-model)

Broadcasty-Lary uses **RDBMS for metadata/audit** and **Redis for presence + replay** (by default). Below is a precise map of what’s stored where, how to tune it, and what to change for production.

### 1) RDBMS Tables (migrations included)

[](#1-rdbms-tables-migrations-included)

Engine recommendation:

- **MySQL 8+/MariaDB 10.6+** or **PostgreSQL 13+**
- Charset/Collation: `utf8mb4` (MySQL) or `UTF8` (PostgreSQL)
- Row format: dynamic/compact (MySQL InnoDB)

**a) `broadcasty_channels`**

- Purpose: optional per-channel policy snapshot.
- Columns: `tenant_id (index)`, `name (index, unique with tenant)`, `policy (JSON)`, timestamps.
- Indexes: unique `(tenant_id, name)`; both are already indexed.
- Notes: if channels are ephemeral, you can leave this empty.

**b) `broadcasty_api_keys`**

- Purpose: service-to-service access (if you extend the package with HMAC/API-key flows).
- Columns: `tenant_id (index)`, `name`, `key (unique)`, `hash (unique)`, `scopes (JSON)`, `rotated_at`, timestamps.
- Rotation: use `php artisan broadcasty:key:rotate` to integrate with Vault/KMS in your app.

**c) `broadcasty_events`**

- Purpose: optional durable audit/replay mirror **(disabled by default)**; by default, replay lives in Redis for performance.
- Columns: `tenant_id (index)`, `channel (index)`, `partition (index)`, `sequence (index)`, `envelope_id (idempotency)`, `payload (BLOB/BYTEA)`, `meta (JSON)`, `published_at (index)`.
- Uniqueness: `(tenant_id, channel, partition, sequence)`.
- When to enable: only if you need **long-term retention**, compliance/regulatory replay, or cross-region ETL. For most cases, rely on Redis replay + compact retention.
- Tuning: partition by tenant or channel if this grows large; add composite indexes for typical queries.

**d) `broadcasty_audit_logs`**

- Purpose: action trails (publish, presence, admin actions).
- Columns: `tenant_id (index)`, `action`, `actor_id`, `ip`, `ctx (JSON)`, timestamps.
- Rotation: schedule deletion/archival every N days (see rotation section).

> **Table prefixing**: Laravel supports a per-connection prefix in `config/database.php` (e.g. `'prefix' => 'bcy_'`). The bundled migration uses explicit table names. If you need a table prefix, either (1) configure the DB connection prefix, or (2) edit the migration file names to include your prefix. We don’t force a custom prefix from `.env` to avoid conflicts.

#### Suggested extra indexes (optional, heavy traffic)

[](#suggested-extra-indexes-optional-heavy-traffic)

MySQL:

```
CREATE INDEX bcy_evt_tenant_chan_pub ON broadcasty_events (tenant_id, channel, published_at);
CREATE INDEX bcy_audit_tenant_time ON broadcasty_audit_logs (tenant_id, created_at);
```

PostgreSQL:

```
CREATE INDEX bcy_evt_tenant_chan_pub ON broadcasty_events (tenant_id, channel, published_at);
CREATE INDEX bcy_audit_tenant_time ON broadcasty_audit_logs (tenant_id, created_at);
```

### 2) Redis Keyspaces (default)

[](#2-redis-keyspaces-default)

- Presence store: `bcy:prs:{tenant}:{channel}` → `HSET memberId -> JSON(member)`; TTL refreshed on join/heartbeat, base TTL from `config('broadcasty.presence.ttl')`.
- Sequences: `bcy:seq:{tenant}:{channel}:{partition}` → `INCR` for strict ordering.
- Replay store: `bcy:rpl:{tenant}:{channel}:{partition}` → `HSET sequence -> payload` with `EXPIRE` = `replay.retention_sec`.

> **Isolation &amp; multi-tenant**: `BROADCASTY_REDIS_PREFIX` defaults to `bcy:`. If your Redis is shared, change the prefix and keep non-overlapping tenant IDs.

### 3) Replay retention &amp; compaction

[](#3-replay-retention--compaction)

Config: `config('broadcasty.replay')`

- `retention_sec`: how long per-partition data stays in Redis (default 3600s).
- `max_bytes_per_channel`: soft target if you implement your own compactor.
- `partitions`: number of partitions per channel from the policy resolver.

**Compaction &amp; rotation**: the included `RedisReplayStore` relies on Redis `EXPIRE`. For long-term retention, either:

- Mirror events into `broadcasty_events` (toggle in your app’s publisher path), or
- Export Redis replay to another store on a schedule (queue job).

### 4) Migrations in production (zero-downtime tips)

[](#4-migrations-in-production-zero-downtime-tips)

- Use `php artisan migrate` behind maintenance mode or deploy windows.
- Add new large indexes concurrently (PostgreSQL `CREATE INDEX CONCURRENTLY`, MySQL pt-online-schema-change or gh-ost).
- For very large `broadcasty_events`, prefer partitioned tables (PostgreSQL declarative partitioning by month/tenant; MySQL 8 range/list partitioning).

### 5) Backup &amp; restore

[](#5-backup--restore)

- DB: include `broadcasty_*` tables in your normal backups.
- Redis: snapshotting (RDB) or AOF; ensure persistence if replay durability matters.
- Test restores for a representative tenant/channel before enabling strict SLAs.

---

### Operational Housekeeping

[](#operational-housekeeping)

#### Rotations &amp; retention

[](#rotations--retention)

- **JWT/public keys**: rotate regularly via your IdP/secret manager, then refresh `BROADCASTY_JWT_PUBLIC_KEY`.
- **API keys**: `php artisan broadcasty:key:rotate` (extend the command to your KMS/Vault).
- **Audit logs**: add a scheduled task to delete/archive rows older than N days:

```
// app/Console/Kernel.php
$schedule->call(fn() => \DB::table('broadcasty_audit_logs')->where('created_at','
