7amoood / laravel-appsync-broadcaster
AWS AppSync broadcaster for Laravel Broadcasting - Real-time communication using AWS AppSync Events API
Package info
github.com/7amoood/laravel-appsync-broadcaster
pkg:composer/7amoood/laravel-appsync-broadcaster
Requires
- php: ^8.1
- clue/redis-react: ^2.7
- illuminate/broadcasting: ^10.0|^11.0|^12.0|^13.0
- illuminate/cache: ^10.0|^11.0|^12.0|^13.0
- illuminate/console: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/redis: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- nesbot/carbon: ^2.0|^3.0
- ratchet/pawl: ^0.4
- react/event-loop: ^1.5
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.0|^12.0|^13.0
README
A Laravel broadcasting driver for AWS AppSync that supports two publishing modes (direct, redis) and two transports (http, websocket) for real-time event delivery.
Features
- Two publishing modes: publish directly from Laravel or queue through Redis
- Two transport layers: send via HTTP or a persistent WebSocket connection
- Background worker support:
php artisan appsync:workerrelays Redis events to AppSync - Channel support: public, private, and presence channels
- Authentication: Cognito-based auth for secured channels
- Retry, buffering, and back-pressure controls
- Frontend integration: WebSocket subscription examples for JavaScript and browsers
- Laravel integration: plugs into Laravel's native broadcasting system
Installation
Install the package via Composer:
composer require 7amoood/laravel-appsync-broadcaster
The service provider is auto-discovered by Laravel.
Configuration
- Publish the configuration file:
php artisan vendor:publish --tag=appsync-config
- Set AppSync as your default broadcast driver and configure the core variables in
.env:
BROADCAST_DRIVER=appsync APPSYNC_NAMESPACE=default APPSYNC_APP_ID=your-appsync-app-id APPSYNC_EVENT_REGION=us-east-1 # Publishing architecture APPSYNC_MODE=direct APPSYNC_TRANSPORT=http # Cognito OAuth2 credentials APPSYNC_COGNITO_SCOPE= APPSYNC_COGNITO_POOL=your-cognito-pool-id APPSYNC_COGNITO_REGION=us-east-1 APPSYNC_COGNITO_CLIENT_ID=your-cognito-client-id APPSYNC_COGNITO_CLIENT_SECRET=your-cognito-client-secret
- Optional tuning values from
config/appsync.php:
# Cache / token APPSYNC_CACHE_STORE=redis APPSYNC_CACHE_PREFIX=appsync_broadcast_ APPSYNC_TOKEN_BUFFER=120 # HTTP transport APPSYNC_HTTP_TIMEOUT=10 APPSYNC_HTTP_CONNECT_TIMEOUT=5 APPSYNC_RETRY_MAX_ATTEMPTS=3 APPSYNC_RETRY_BASE_DELAY=200 APPSYNC_RETRY_MAX_DELAY=5000 APPSYNC_RETRY_MULTIPLIER=2.0 # Redis mode APPSYNC_REDIS_CONNECTION=default APPSYNC_REDIS_CHANNEL=appsync:events APPSYNC_REDIS_BATCH_SIZE=50 APPSYNC_REDIS_FLUSH_INTERVAL=100 # WebSocket transport APPSYNC_WS_BATCH_SIZE=5 APPSYNC_WS_FLUSH_INTERVAL=50 APPSYNC_WS_MAX_BUFFER=10000 APPSYNC_WS_KA_TIMEOUT_MULTIPLIER=1.5 APPSYNC_WS_STATS_INTERVAL=30
Note: The AppSync connection is registered automatically. You do not need to manually add it to
config/broadcasting.php.
Modes
| Mode | Description | Worker required |
|---|---|---|
direct |
Laravel publishes straight to AppSync using the configured transport | No |
redis |
Laravel publishes to Redis, and a worker relays the events to AppSync | Yes |
Transports
| Transport | Description | Best for |
|---|---|---|
http |
Sends one or more events using HTTP requests to AppSync | Simple/default setup |
websocket |
Uses a persistent WebSocket connection with batching | Lower overhead / higher throughput |
Supported combinations
APPSYNC_MODE |
APPSYNC_TRANSPORT |
Behavior |
|---|---|---|
direct |
http |
Laravel sends events directly over HTTP |
direct |
websocket |
Laravel sends events directly over WebSocket |
redis |
http |
Laravel publishes to Redis, worker flushes to AppSync over HTTP |
redis |
websocket |
Laravel publishes to Redis, worker relays over persistent WebSocket |
Worker Usage (redis mode)
If you choose APPSYNC_MODE=redis, you must run the worker process:
php artisan appsync:worker
Common overrides:
php artisan appsync:worker --transport=websocket php artisan appsync:worker --batch-size=100 --flush-interval=200 php artisan appsync:worker --redis-channel=appsync:events --stats-interval=30
Production note: Run the worker under Supervisor or systemd in production.
Usage
Basic Broadcasting
Use Laravel's standard broadcasting flow:
class OrderUpdated implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; public $order; public function __construct(Order $order) { $this->order = $order; } public function broadcastOn() { return new PrivateChannel('orders/' . $this->order->id); } } event(new OrderUpdated($order));
Presence Channels
For presence channels, use Laravel's PresenceChannel as usual:
public function broadcastOn() { return new PresenceChannel('chat/room/' . $this->roomId); }
How publishing works
directmode: your Laravel request publishes to AppSync immediately.redismode: your Laravel request only pushes the event to Redis, and the worker handles delivery.httptransport: uses HTTP requests with retry and exponential backoff.websockettransport: keeps a live connection open and batches events efficiently.
Channel Authentication
For private and presence channels, define authorization logic in routes/channels.php:
// Private channel authorization Broadcast::channel('orders/{orderID}', function ($user, $orderID) { return (int) $user->id === (int) Order::find($orderID)->user_id; }); // Presence channel authorization Broadcast::channel('chat/room/{roomId}', function ($user, $roomId) { if ($user->canAccessRoom($roomId)) { return ['id' => $user->id, 'name' => $user->name]; } });
Frontend Integration
To subscribe to channels from your frontend application, you need to establish a WebSocket connection to AWS AppSync. Here's how to do it:
JavaScript/Node.js Example
const WebSocket = require("ws"); const axios = require("axios"); // Your AppSync configuration const API_ID = "your-appsync-app-id"; const REGION = "your-region"; // e.g., "us-east-1" const TOKEN = 'your-user-token'; // JWT token for authenticated users const CHANNEL = "default/private-orders/123"; // Channel to subscribe to // AppSync endpoints const AUTH_URL = 'https://your-app.com/broadcasting/auth'; const REALTIME_DOMAIN = `${API_ID}.appsync-realtime-api.${REGION}.amazonaws.com`; const HTTP_DOMAIN = `${API_ID}.appsync-api.${REGION}.amazonaws.com`; async function connectAppSync() { try { // Step 1: Authenticate with your Laravel app to get AppSync API key const res = await axios.post(AUTH_URL, { channel_name: CHANNEL, }, { headers: { Authorization: `Bearer ${TOKEN}` } }); const API_KEY = res.data.auth; console.log("API_KEY obtained:", API_KEY); // Step 2: Connect to AppSync WebSocket const headerObj = { host: HTTP_DOMAIN, Authorization: API_KEY }; // Base64 encode headers (URL safe) const header = Buffer.from(JSON.stringify(headerObj)) .toString("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); const url = `wss://${REALTIME_DOMAIN}/event/realtime`; const ws = new WebSocket(url, [`header-${header}`, "aws-appsync-event-ws"]); // Step 3: Handle connection ws.on("open", () => { console.log("✅ Connected to AppSync Realtime"); ws.send(JSON.stringify({ type: "connection_init" })); }); // Step 4: Handle messages ws.on("message", (msg) => { const data = JSON.parse(msg.toString()); console.log("📩 Message:", data); // Handle connection acknowledgement if (data.type === "connection_ack") { console.log("✅ Connection ACK received, subscribing to channel"); // Subscribe to the channel ws.send(JSON.stringify({ id: crypto.randomUUID(), // Generate unique subscription ID type: "subscribe", channel: CHANNEL, authorization: { "Authorization": API_KEY, "host": HTTP_DOMAIN } })); } // Handle incoming data if (data.type === "data") { const event = JSON.parse(data.event); console.log("📦 Event received:", event); // Handle your event data switch (event.event) { case 'OrderUpdated': console.log('Order updated:', event.data); break; case 'UserMessage': console.log('New message:', event.data); break; // Add more event handlers as needed } } }); // Handle disconnection ws.on("close", () => console.log("❌ Disconnected")); ws.on("error", (err) => console.error("⚠️ Error:", err)); } catch (err) { console.error("Error connecting to AppSync:", err.message); } } // Start the connection connectAppSync();
Browser Example (ES6+)
class AppSyncSubscriber { constructor(config) { this.apiId = config.apiId; this.region = config.region; this.authUrl = config.authUrl; this.token = config.token; this.ws = null; this.subscriptions = new Map(); } async connect() { const realtimeDomain = `${this.apiId}.appsync-realtime-api.${this.region}.amazonaws.com`; const httpDomain = `${this.apiId}.appsync-api.${this.region}.amazonaws.com`; try { // Get API key from Laravel const response = await fetch(this.authUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}` }, body: JSON.stringify({ channel_name: 'default' }) }); const { auth: apiKey } = await response.json(); // Create WebSocket connection const headerObj = { host: httpDomain, Authorization: apiKey }; const header = btoa(JSON.stringify(headerObj)) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); this.ws = new WebSocket( `wss://${realtimeDomain}/event/realtime`, [`header-${header}`, "aws-appsync-event-ws"] ); this.ws.onopen = () => { console.log("Connected to AppSync"); this.ws.send(JSON.stringify({ type: "connection_init" })); }; this.ws.onmessage = (event) => { const data = JSON.parse(event.data); this.handleMessage(data, apiKey, httpDomain); }; this.ws.onclose = () => console.log("Disconnected"); this.ws.onerror = (err) => console.error("WebSocket error:", err); } catch (error) { console.error("Connection failed:", error); } } handleMessage(data, apiKey, httpDomain) { if (data.type === "connection_ack") { this.onReady && this.onReady(); } else if (data.type === "data") { const event = JSON.parse(data.event); this.onEvent && this.onEvent(event); } } subscribe(channel, callback) { const id = crypto.randomUUID(); this.subscriptions.set(id, callback); this.ws.send(JSON.stringify({ id, type: "subscribe", channel, authorization: { "Authorization": this.apiKey, "host": this.httpDomain } })); return id; // Return subscription ID for unsubscribing } unsubscribe(subscriptionId) { this.ws.send(JSON.stringify({ id: subscriptionId, type: "unsubscribe" })); this.subscriptions.delete(subscriptionId); } } // Usage const subscriber = new AppSyncSubscriber({ apiId: 'your-appsync-app-id', region: 'us-east-1', authUrl: '/broadcasting/auth', token: 'your-jwt-token' }); subscriber.onReady = () => { console.log("Ready to subscribe!"); // Subscribe to channels subscriber.subscribe('default/private-orders/123', (event) => { console.log('Order event:', event); }); }; subscriber.onEvent = (event) => { console.log('Received event:', event); }; subscriber.connect();
Channel Name Format
Channels follow this format: {namespace}/{channel_type}-{channel_name}
- namespace: From your
APPSYNC_NAMESPACEconfig (default: "default") - channel_type:
publicfor public channelsprivatefor private channelspresencefor presence channels
- channel_name: Your channel identifier
Examples:
default/orders(public channel)default/private-user/123(private channel)default/presence-chat/room1(presence channel)
Authentication Flow
- Frontend sends channel name to your Laravel app's
/broadcasting/authendpoint - Laravel validates the request and returns an AppSync API key
- Frontend uses the API key to establish WebSocket connection
- Frontend subscribes to specific channels using the authenticated connection
Event Structure
Events received from AppSync have this structure:
{ event: "EventName", // The broadcast event name channel: "default/orders", // Channel name data: { // Your event data order_id: 123, status: "completed" } }
AWS AppSync Setup
- Create an AppSync Event API in your AWS Console
- Set up Cognito User Pool for authentication
- Create a Cognito App Client with client credentials flow enabled
- Attach Cognito to AppSync as auth method
Error Handling
The broadcaster includes built-in protections for both direct and worker-based delivery:
- HTTP retry logic with exponential backoff and jitter
- Token refresh handling for expired or invalid auth tokens
- Partial failure handling so one failed channel does not stop the rest
- WebSocket reconnect/buffering support for long-lived connections
- Worker buffer limits and stats logging in
redismode
Logging
Important lifecycle and failure details are logged through Laravel's logging system, including:
- Authentication failures
- HTTP retry attempts
- WebSocket disconnects/reconnects
- Redis worker publish/drop statistics
- Configuration and transport errors
Requirements
- PHP ^8.1
- Laravel ^10.0|^11.0|^12.0|^13.0
- AWS AppSync API
- AWS Cognito User Pool
- Redis server (only when using
APPSYNC_MODE=redis)
Compatibility
Composer picks the highest Laravel version your PHP runtime allows.
| Laravel | PHP | Supported |
|---|---|---|
| 13.x | 8.3, 8.4 | ✅ |
| 12.x | 8.2, 8.3, 8.4 | ✅ |
| 11.x | 8.2, 8.3, 8.4 | ✅ |
| 10.x | 8.1, 8.2, 8.3 | ✅ |
Laravel 9 and below are no longer supported; pin ^1.0 of this package if you
need them.
Installing on Laravel 10 or 11: both branches are past their security-EOL, and every release in them carries an unpatched advisory. Recent Composer versions refuse to install advisory-affected packages by default, so a fresh install pinned to
laravel/framework:^10.0or^11.0will fail to resolve — this package is not the cause. Existing lock files are unaffected. To install anyway, opt out withcomposer config policy.advisories.block false. Upgrading to Laravel 12 or 13 is the better answer.
License
This package is open-sourced software licensed under the MIT license.
Contributing
Please see CONTRIBUTING for details.
Security
If you discover any security-related issues, please email the package maintainer instead of using the issue tracker.