Broadcasting (Reverb)
Postbox nutzt Laravel Reverb als First-Party WebSocket-Server fuer Echtzeit-Kommunikation. Events werden server-seitig dispatcht und im Browser ueber Laravel Echo empfangen.
Architektur
Browser (wss://app.postbox.so:443/app)
└─> Nginx (Reverse Proxy)
└─> Reverb (127.0.0.1:8081)
Laravel (Server-seitig)
└─> http://127.0.0.1:8081 (direkt, ohne Cloudflare)
Server-seitiges Broadcasting verbindet direkt zu Reverb auf localhost, um Cloudflare und andere Proxies zu umgehen.
.env Konfiguration
Server-seitig (Laravel zu Reverb)
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=postbox
REVERB_APP_KEY=your-random-key
REVERB_APP_SECRET=your-random-secret
REVERB_SERVER_HOST=127.0.0.1
REVERB_SERVER_PORT=8081
Client-seitig (Browser zu Nginx zu Reverb)
REVERB_HOST=app.postbox.so
REVERB_PORT=443
REVERB_SCHEME=https
Frontend (Vite)
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
Nginx Konfiguration
Das /app-Prefix muss auf den Reverb-Server proxied werden:
location /app {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header SERVER_PORT $server_port;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://127.0.0.1:8081;
}
Forge Daemon
Reverb muss als Daemon-Prozess laufen. In Laravel Forge unter Server - Daemons:
| Feld | Wert |
|---|---|
| Command | php8.4 artisan reverb:start --host=0.0.0.0 --port=8081 |
| Directory | /home/forge/app.postbox.so/current |
| User | forge |
| Processes | 1 |
Broadcast Channels
Location: routes/channels.php
| Channel | Typ | Autorisierung | Events |
|---|---|---|---|
user.{userId} | Private | (int) $user->id === (int) $userId | YouTube Sync, Daily Scrape, Profile Sanitized/Unsanitized/Reactivated |
workspace.{workspaceId} | Private | $user->workspaces()->where('workspaces.id', $workspaceId)->exists() | Import Progress |
profile.{profileId} | Private | User hat Watcher mit diesem Profil (ueber WatcherSource + Workspace) | Related Profiles Calculated |
admin | Private | $user->isAdmin() | User Registered, Server Alerts, Server Metrics |
test-channel | Public | Keine | Test Broadcast (Admin-Seite) |
Channel Authorization Code
// routes/channels.php
Broadcast::channel('user.{userId}', fn($user, $userId) =>
(int) $user->id === (int) $userId
);
Broadcast::channel('workspace.{workspaceId}', fn($user, $workspaceId) =>
$user->workspaces()->where('workspaces.id', $workspaceId)->exists()
);
Broadcast::channel('profile.{profileId}', fn($user, $profileId) =>
WatcherSource::query()
->where('social_profile_id', $profileId)
->whereHas('watcher', fn($q) => $q
->whereIn('workspace_id', $user->workspaces->pluck('id')))
->exists()
);
Broadcast::channel('admin', fn($user) => $user->isAdmin());
Events
Location: app/Events/
Event-Uebersicht
| Event | Channel(s) | broadcastAs | Interface | Beschreibung |
|---|---|---|---|---|
WatcherImportProgress | workspace.{id} | import.progress | ShouldBroadcast | Import-Fortschritt pro URL (status, counters) |
YouTubeVideoSyncCompleted | user.{userId} | youtube.sync.completed | ShouldBroadcastNow | YouTube Video-Stats Sync fertig |
DailyScrapeCompleted | user.{userId} | daily.scrape.completed | ShouldBroadcast | Taegliche Aktualisierung abgeschlossen |
RelatedProfilesCalculated | profile.{id} + user.{id} | related.profiles.calculated | ShouldBroadcast | Related-Berechnung abgeschlossen |
ProfileReactivated | user.{userId} (alle betroffenen) | profile.reactivated | ShouldBroadcast | Deaktiviertes Profil reaktiviert |
ProfileSanitized | user.{userId} (alle betroffenen) | profile.sanitized | ShouldBroadcast | Profil durch Sanitizer deaktiviert |
ProfileUnsanitized | user.{userId} (alle betroffenen) | profile.unsanitized | ShouldBroadcast | Sanitized Profil wieder aktiviert |
UserRegistered | admin | user.registered | ShouldBroadcast | Neuer User registriert |
ServerAlertTriggered | admin | server.alert.triggered | ShouldBroadcastNow | Server-Metrik ueber Schwellwert |
ServerMetricsUpdated | admin | server.metrics.updated | ShouldBroadcastNow | Server-Metriken Update (~15s) |
TestBroadcast | test-channel | toast.show | ShouldBroadcastNow | Test-Event fuer Reverb-Konnektivitaet |
SuppressesBroadcastFailures Pattern
Broadcasts sind "nice to have" -- wenn Reverb temporaer nicht erreichbar ist, soll die App weiter funktionieren. Alle ShouldBroadcast-Events (ausser TestBroadcast und ShouldBroadcastNow-Events) nutzen das Trait SuppressesBroadcastFailures.
Location: app/Events/Concerns/SuppressesBroadcastFailures.php, app/Queue/Middleware/SuppressBroadcastFailures.php
Funktionsweise
// app/Events/Concerns/SuppressesBroadcastFailures.php
trait SuppressesBroadcastFailures
{
public function middleware(): array
{
return [
new \App\Queue\Middleware\SuppressBroadcastFailures,
];
}
}
// app/Queue/Middleware/SuppressBroadcastFailures.php
class SuppressBroadcastFailures
{
public function handle(object $job, Closure $next): void
{
try {
$next($job);
} catch (BroadcastException $e) {
Log::warning('Broadcast failed (Reverb unavailable)', [
'job' => get_class($job),
'message' => $e->getMessage(),
]);
}
}
}
Verwendung in Events
class WatcherImportProgress implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels, SuppressesBroadcastFailures;
// ...
}
Bei Reverb-Ausfall
- Queue-Jobs failen nicht mehr wegen
BroadcastException - Stattdessen wird eine Warning geloggt
- Die eigentliche Funktionalitaet (Job) laeuft weiter
- Nur die Echtzeit-Benachrichtigung entfaellt
- Zusaetzlich:
BroadcastExceptionindontReport(Backup inbootstrap/app.php)
NotificationCenter Integration
Location: app/Livewire/NotificationCenter.php, resources/views/livewire/notification-center.blade.php
Event-Flow
1. Laravel Event wird dispatched
└─> Reverb broadcastet an Channel
2. Laravel Echo empfaengt Event im Browser
└─> Ruft $wire.dispatch('reverb-*') auf
3. Livewire NotificationCenter Handler verarbeitet Event
└─> Persistenz in Nachrichtenzentrale (falls konfiguriert)
4. Handler dispatcht 'show-toast' Event
└─> JavaScript zeigt window.Flux.toast() an
5. 'notification-received' Event wird dispatcht
└─> NotificationBell aktualisiert Unread Count
Abonnierte Channels pro User
user.{userId} // YouTube Sync, Daily Scrape, Profile Events
workspace.{id} // Import Progress (fuer jeden Workspace des Users)
admin // User Registrations, Server Alerts (nur Admins)
profile.{id} // Related Profiles (dynamisch)
Rate-Limiting
Der NotificationCenter verwendet safeServerDispatch() mit maximal 40 Calls pro Burst, um den Server bei vielen gleichzeitigen Events nicht zu ueberlasten.
Toast-Typen
| Typ | Heading | Verwendung |
|---|---|---|
success | Erfolg | Erfolgreiche Aktionen (Speichern, Import fertig) |
warning | Warnung | Teilweiser Erfolg, Warnungen |
danger / error | Fehler | Fehlgeschlagene Aktionen |
info | Info | Informationsmeldungen (Default) |
Toast in Livewire dispatchen
// Erfolg (gruen)
$this->dispatch('show-toast', ['message' => 'Gespeichert', 'type' => 'success']);
// Fehler (rot)
$this->dispatch('show-toast', ['message' => 'Fehler aufgetreten', 'type' => 'error']);
// Info (blau)
$this->dispatch('show-toast', ['message' => 'Wird verarbeitet...', 'type' => 'info']);
// Warnung (gelb)
$this->dispatch('show-toast', ['message' => 'Limit erreicht', 'type' => 'warning']);
Manuell testen
Admin-Seite
Die Route /admin/reverb-test bietet Verbindungsstatus, Test-Broadcast und Event-Log.
CLI
# Test Broadcast (oeffentlicher Channel)
php artisan tinker --execute='App\Events\TestBroadcast::dispatch("Hello!", "success");'
# Import Progress (Workspace 1)
php artisan tinker --execute='event(new \App\Events\WatcherImportProgress(1, 1, "finished", "Test!", 5, 10, 2));'
# YouTube Sync (User 1)
php artisan tinker --execute='event(new \App\Events\YouTubeVideoSyncCompleted(1, "testchannel", true, 25));'
# Daily Scrape (User 1)
php artisan tinker --execute='event(new \App\Events\DailyScrapeCompleted(1, 10, 10));'
Relevante Dateien
| Datei | Beschreibung |
|---|---|
config/broadcasting.php | Laravel Broadcasting Konfiguration |
config/reverb.php | Reverb Server Konfiguration |
routes/channels.php | Channel Authorization |
resources/js/echo.js | Laravel Echo Frontend Setup |
app/Events/ | Alle Broadcast Events |
app/Events/Concerns/SuppressesBroadcastFailures.php | Graceful Degradation Trait |
app/Queue/Middleware/SuppressBroadcastFailures.php | Queue Middleware |
app/Livewire/NotificationCenter.php | Notification Center (Server) |
app/Livewire/NotificationBell.php | Bell Component (Unread Count) |
resources/views/livewire/notification-center.blade.php | Notification Center (Client, Echo Subscriptions) |