7amoood / laravel-appsync-broadcaster
AWS AppSync broadcaster for Laravel Broadcasting - Real-time communication using AWS AppSync GraphQL subscriptions
Requires
- php: ^8.0
- illuminate/broadcasting: ^9.0|^10.0|^11.0
- illuminate/http: ^9.0|^10.0|^11.0
- illuminate/support: ^9.0|^10.0|^11.0
- nesbot/carbon: ^2.0|^3.0
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0|^9.0
- phpunit/phpunit: ^9.0|^10.0
README
A Laravel Broadcasting driver for AWS AppSync that enables real-time communication using AWS AppSync GraphQL subscriptions.
Features
- Real-time Broadcasting: Leverage AWS AppSync for real-time messaging
- Channel Support: Support for public, private, and presence channels
- Authentication: Cognito-based authentication for private channels
- Frontend Integration: WebSocket connection examples for JavaScript/Node.js
- Retry Logic: Built-in retry mechanism with exponential backoff
- Error Handling: Comprehensive error handling and logging
- Laravel Integration: Seamless integration with Laravel's Broadcasting system
Installation
Install the package via Composer:
composer require 7amoood/laravel-appsync-broadcaster
The service provider will be automatically registered due to Laravel's package auto-discovery.
Configuration
- Publish the configuration file:
php artisan vendor:publish --tag=appsync-config
- Add the following environment variables to your
.env
file:
APPSYNC_NAMESPACE=your-app-namespace-or-remove-it-to-set-default APPSYNC_APP_ID=your-appsync-app-id APPSYNC_EVENT_REGION=us-east-1 APPSYNC_COGNITO_POOL=your-cognito-pool-id APPSYNC_COGNITO_REGION=us-east-1-or-remove-it-for-same APPSYNC_COGNITO_CLIENT_ID=your-cognito-client-id APPSYNC_COGNITO_CLIENT_SECRET=your-cognito-client-secret
- Set AppSync as your default broadcast driver in
.env
:
BROADCAST_DRIVER=appsync
Note: The AppSync broadcasting connection is automatically registered. You don't need to manually add it to your
config/broadcasting.php
file.
Usage
Basic Broadcasting
Use Laravel's standard broadcasting features:
// In your event class 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); } } // Dispatch the event event(new OrderUpdated($order));
Presence Channels
For presence channels, use the presence-
prefix:
public function broadcastOn() { return new PresenceChannel('chat/room/' . $this->roomId); }
Manual Broadcasting
You can also broadcast manually:
use Illuminate\Support\Facades\Broadcast; Broadcast::channel('test-channel')->send('test-event', [ 'message' => 'Hello World!' ]);
Channel Authentication
For private and presence channels, you need to define authorization logic in your 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_NAMESPACE
config (default: "default") - channel_type:
public
for public channelsprivate
for private channelspresence
for 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/auth
endpoint - 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 comprehensive error handling:
- Connection failures: Automatic retry with exponential backoff
- Authentication errors: Detailed logging and error messages
- Partial failures: Continues broadcasting to other channels if some fail
- Configuration validation: Validates required configuration parameters
Logging
All errors and important events are logged using Laravel's logging system. Check your logs for:
- Authentication failures
- Broadcast failures
- Retry attempts
- Configuration errors
Requirements
- PHP ^8.0
- Laravel ^9.0|^10.0|^11.0
- AWS AppSync API
- AWS Cognito User Pool
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.