1. What you need, and what you don’t
2. How the pieces talk
Solid lines exist in both modes. Dotted lines exist only in live mode (BROADCAST_DRIVER=reverb and a running Reverb process). Port 8080 never leaves the machine: nginx and Laravel reach it on 127.0.0.1.
What happens when the instructor sends a message:
3. Install on a server (DEV, TEST, PROD)
Do these steps once per server, over SSH, as the site’s system user (Plesk → the domain → Hosting settings shows it). At the end the chatrooms work in polling mode.1
Open the backend folder and pick the right PHP
2
Get the code
3
Install the packages
pcntl and sockets PHP extensions are recommended for the Reverb process ($PHP -m | grep -E "pcntl|sockets"), not required.4
Add the chatroom block to .env
Open the server’s Generate the three Reverb values, one set per environment (never reuse PROD’s on TEST). They are just random strings, so they don’t have to be generated on the server: your own computer works as well, which is the easy way when SSH on the server is limited.Paste them without SSH. Plesk → Files → open the backend folder →
Leave the rest of
.env and add this block at the end. Do not add a second BROADCAST_DRIVER line: if the file already has one, delete it and keep the one in this block (section 4.1 explains why).- On your computer (no SSH needed)
- On the server (SSH)
In a macOS or Linux terminal, this prints the three lines ready to paste, with only lowercase letters and digits (what Reverb and the If PHP gives you trouble, this version uses only Run it once per environment, so TEST and PROD get different values. Don’t save the output anywhere else, and don’t paste it into a chat or a ticket.
.env file need):openssl:.env → Edit in Text Editor. Replace the three empty REVERB_APP_* lines with the printed ones and save. Then clear the config cache (next steps; without SSH, see “Run an artisan command without SSH” in the cache step).Why the PHP command fails on the server
Why the PHP command fails on the server
Two common causes, and the error tells you which one:
-r: command not found:$PHPis empty. The linePHP=/opt/plesk/php/8.2/bin/php(step 1) has to run first in the same SSH session; a new session forgets it. Plainphp -r '…'also works when the systemphpis recent enough.No such file or directoryfor/opt/plesk/php/8.2/bin/php: the SSH shell is chrooted, a locked-down shell that cannot see Plesk’s PHP. A Plesk admin can change it under the domain’s Hosting settings: set “Access to the server over SSH” to/bin/bashinstead of/bin/bash (chrooted). Generating the keys on your computer avoids the problem altogether.
.env as it is. Do not run php artisan reverb:install: it appends its own variables to .env and rewrites config/broadcasting.php, and everything it does is already in the code.5
Create the tables
Run
migrate on the server: run from a laptop it migrates the laptop’s database. Rollback, if ever needed: $PHP artisan migrate:rollback --step=2 (drops the four chatroom tables with their messages).6
Clear the caches and restart PHP
systemctl restart plesk-php82-fpm (PHP-FPM; a domain on FastCGI under Apache needs an Apache restart instead). Do this after every pull and every .env change: a cached config ignores .env until optimize runs again.Run an artisan command without SSH. Plesk → Scheduled Tasks → Add Task → Run a command, with the line below, then Run Now. Delete the task afterwards. The same trick works for optimize and the other artisan commands of this page. If your Plesk has the Laravel Toolkit, its Artisan tab does the same thing.7
Allow 20 MB images
- Plesk → the domain → PHP Settings:
upload_max_filesize = 25Mandpost_max_size = 25M. - Plesk → the domain → Apache & nginx Settings → Additional nginx directives:
413 (nginx) or 422 “failed to upload” (PHP).8
Run the scheduler
Plesk → Scheduled Tasks → Add task → Run a command, every minute (It runs
* * * * *), as the site’s user:chatrooms:prune-attachments every night (images uploaded but never sent, older than 24 hours) and the existing notification clean-up.9
Check
POST /api/auth/login; use a test account, never paste a real password into a shared script):Testing by hand with
curl? Always send a body on a POST (-d '{}'). The Plesk web firewall refuses a body-less POST with an HTML 403 Forbidden page, and a few of those get your IP banned for a while (every request then times out). The apps always send a body, so they are not affected. Unban in Plesk → Tools & Settings → IP Address Banning.4. The .env, explained
4.1 One line per key
Laravel reads.env from top to bottom and the last line wins. A file with BROADCAST_DRIVER=reverb near the top and BROADCAST_DRIVER=log in the chatroom block runs with log. Keep exactly one line per key; the same applies to any other key that appears twice.
4.2 What each variable means
Optional, the defaults are fine:
REVERB_APP_PING_INTERVAL (60 s), REVERB_APP_ACTIVITY_TIMEOUT (30 s), REVERB_APP_MAX_MESSAGE_SIZE (10 000 bytes; chat events stay under it, a long message is sent as its id and the apps fetch it), REVERB_MAX_REQUEST_SIZE (100 000 bytes).
4.3 Keys the chatrooms rely on but that already exist
5. Make messages instant: run Reverb
Without this section the chat works by polling. With it a message reaches the other side in milliseconds.1
Try Reverb by hand
.env still says log; it works when the config is not cached, otherwise do this check after the switch step):private-chatroom.0. Stop it with Ctrl+C. Never use --debug under supervisor (it prints every frame).2
Keep it running
Plesk’s Scheduled Tasks cannot keep a process alive, so this needs root once. Pick one:One program per environment. When two environments share a server, give each its own port (for example TEST
- supervisor
- systemd
8080, DEV 8081) and use it in that environment’s REVERB_SERVER_PORT, REVERB_PORT and nginx block.3
Proxy the WebSocket through nginx
Plesk → Websites & Domains → the backend domain → Apache & nginx Settings → Additional nginx directives.That box is one field for the whole vhost. It holds the upload limit of the “Allow 20 MB images” step, the A plain
/storage/ CORS block that lets the Flutter web app draw local-storage images (see File Management & Storage), and the chatroom block. The two location blocks do not conflict: each one handles its own path. Add only what is missing; the full box reads:^~makes nginx stop at that prefix, so none of Plesk’s pattern-based locations (such as “Serve static files directly by nginx”) takes the request first. A plainlocation /app/works in practice too;^~is the safer form.X-Forwarded-Forpasses the browser’s address on to Reverb, andproxy_send_timeoutgives sends the same 120 s as reads.8080isREVERB_SERVER_PORT; use the environment’s own port when several share a server.- Plesk checks the syntax and reloads nginx when you press OK.
curl -i without the upgrade headers answers 500 "Internal server error." from Reverb once the proxy works: that is expected, Reverb only speaks WebSocket there.Keep port 8080 closed in the firewall; only nginx and Laravel on the same machine use it.4
Switch the app to live
In Open the same chatroom in two browsers (a learner and the instructor): a message appears on the other side at once, and the learner’s composer locks the moment the instructor turns Learners can write off. The apps need no rebuild.
.env:BROADCAST_DRIVER=log, then optimize:clear and optimize. The apps poll again within 5 seconds; Reverb can keep running.
6. Redis: not needed, and when it would be
The chatrooms never use Redis today:- Reverb keeps every connection of one process in memory. Redis only comes in with
REVERB_SCALING_ENABLED=true, to pass events between several Reverb processes on several machines behind a load balancer. - Laravel sends the chat events during the request, so no queue (and no Redis queue) is involved.
- Cache and sessions stay on
file.
Only if you ever run several Reverb servers
Only if you ever run several Reverb servers
-
Run a Redis server reachable by every Reverb machine (
apt install redis-server, protected by a password and a firewall). -
In every backend
.env: -
optimize:clear,optimize, thenreverb:restarton every machine.
clue/redis-react), so no PHP Redis extension is needed for this. Using Redis for the cache or the queue is a separate decision, unrelated to the chatrooms.7. Values per environment
8. Developer machine
- The Flutter web app on
localhost:5000withEnvironment.localhostinlib/core/constants/constants.dartconnects tows://127.0.0.1:8080directly (Reverb accepts every origin). - Android emulator:
REVERB_CLIENT_HOST=10.0.2.2, the same host the emulator uses for the API. - After changing the API base URL run
dart run build_runner build -d: the URL is baked into the generated Retrofit client. - Tests never need Reverb:
DB_CONNECTION=sqlite DB_DATABASE=:memory: php artisan test --filter=Chatroom.
9. The Flutter app
Nothing to configure for Reverb: the app asksGET /api/realtime/config when a conversation opens, connects when enabled is true, polls every 5 seconds otherwise, and catches up after every reconnection. What it needs is the build that contains the chatrooms:
Constants.currentEnvironment decides which backend the build talks to; set it before building for a server. A running flutter run needs a full restart (not a hot reload) after pulling provider changes.
10. Troubleshooting
Where to look:
storage/logs/laravel.log (API side, failed broadcasts included) and the supervisor stdout_logfile (Reverb side). The backend repository’s CHATROOMS.md holds the same steps for offline use.
11. Security checklist
.envstays on the server, outside git. This page and the repositories contain placeholders only.- One Reverb key set per environment.
REVERB_APP_KEYis public by design;REVERB_APP_SECRETnever leaves the server. - Reverb listens on
127.0.0.1only; the outside reaches it through nginx and HTTPS. - Every channel subscription is signed by the backend (
POST /api/broadcasting/auth, behind the login) and only members and the company’s admins are admitted. APP_DEBUG=falseon production.- A key that was shared outside the server (chat, ticket, screenshot) is rotated: generate a new value, update
.env,optimize,reverb:restart.