> ## Documentation Index
> Fetch the complete documentation index at: https://theteam-13fc7f49.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Chatrooms Setup Guide

> Everything a backend server needs for the classroom chatrooms: code, .env, database, Laravel Reverb for live messages, nginx, uploads, scheduler, Redis (not needed) and the Flutter side, with every command.

This page takes a server (DEV, TEST or PROD on Plesk) or a developer machine from "chatrooms not installed" to "messages arrive live". It is written for the **current version** of the application:

| | |
| :- | :- |
| Backend | Laravel `10.49`, PHP `8.2`, `laravel/reverb` `1.12.0` (+ `pusher/pusher-php-server` `7.3`) |
| Branch | The chatroom work is on **`chat+ai`** in `LMS_Backend` and `LMS_Frontend` (not merged into `main` yet). Deploy that branch, or `main` once it is merged |
| Flutter | The app build that contains the chatrooms (same branch). Real time uses `web_socket_channel` with an in-house Pusher-protocol client: no Pusher SDK, same code on web, Android and iOS |
| What the feature is | [Chatrooms API](https://theteam-13fc7f49.mintlify.site/api-reference/chatrooms.md) (endpoints, tables in §2.4, events) and [Classroom Chatrooms](https://theteam-13fc7f49.mintlify.site/clean-up/chatrooms.md) (design and decisions) |

> [!WARNING]
>
> **No real value appears on this page.** Every key, secret, password and token below is a placeholder (`<generated>`, `<token>`, …). Generate your own values on each server, keep them in that server's `.env` only, and never commit a `.env` file. If a real `.env` was ever pasted into a chat, a ticket or a screenshot, rotate the keys it holds (R2, mail, payment, JWT, database).

***

## 1. What you need, and what you don't

| Piece | Needed? | Why |
| :- | :- | :- |
| PHP 8.2 for the domain **and** on the command line | ✅ | `composer.json` asks for `^8.2.2`. On Plesk use `/opt/plesk/php/8.2/bin/php`; the system `php` may be another version |
| Composer | ✅ | brings `laravel/reverb` |
| The existing MySQL database | ✅ | two new migrations (four new tables + a data-only fix) |
| Cloudflare R2 (already configured) | ✅ for images | chat images and admin CSV backups go to the `r2` disk |
| PHP / nginx upload limits of 25 MB | ✅ for images | the paperclip accepts images up to 20 MB |
| A cron for `schedule:run` | ✅ | daily clean-up of images uploaded but never sent |
| **Laravel Reverb process** | Optional | without it the apps **poll every 5 seconds** and everything works; with it messages arrive instantly |
| supervisor (or systemd) + an nginx `/app/` block | Only with Reverb | keeps Reverb running and carries `wss://` through the site's certificate |
| **Redis** | ❌ | Reverb only uses Redis to share events between **several** Reverb processes (`REVERB_SCALING_ENABLED=true`). One process holds thousands of connections in memory. See section 6 |
| A queue worker | ❌ | chat events are sent during the request (`ShouldBroadcastNow`); `QUEUE_CONNECTION=sync` stays |
| A new domain or certificate | ❌ | nginx proxies `/app/` on the backend's own domain |
| A Flutter rebuild to turn Reverb on or off | ❌ | the apps read `GET /api/realtime/config` at run time |

***

## 2. How the pieces talk

```mermaid theme={null}
flowchart LR
    subgraph client["Flutter app: web, Android, iOS"]
        APP["Chat screen"]
    end
    subgraph server["Backend server (Plesk)"]
        NGINX["nginx :443<br/>site certificate"]
        API["Laravel API<br/>Apache or PHP-FPM"]
        REVERB["Reverb process<br/>127.0.0.1:8080"]
        DB[("MySQL<br/>chatroom tables")]
        CRON["cron: schedule:run<br/>every minute"]
    end
    R2[("Cloudflare R2<br/>images, CSV backups")]

    APP -->|"HTTPS /api/..."| NGINX
    NGINX --> API
    APP -.->|"WSS /app/key (live mode)"| NGINX
    NGINX -.->|"proxy /app/"| REVERB
    API -->|"events, http 127.0.0.1:8080"| REVERB
    API --> DB
    API --> R2
    CRON --> API
```

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:

```mermaid theme={null}
sequenceDiagram
    participant L as Learner app
    participant RV as Reverb
    participant API as Laravel API
    participant DB as MySQL
    participant I as Instructor app

    L->>API: GET /api/realtime/config
    API-->>L: enabled true, key, public host, port 443
    L->>RV: WSS connect to /app/key
    RV-->>L: socket id
    L->>API: POST /api/broadcasting/auth for private-chatroom.12
    API-->>L: signature, only for members and the company admins
    L->>RV: subscribe to private-chatroom.12 with the signature
    I->>API: POST /api/chatrooms/slug/messages
    API->>DB: insert the message and one in-app notification per learner
    API->>RV: event message.sent
    API-->>I: 201 with the message
    RV-->>L: message.sent on private-chatroom.12
    Note over L,API: Polling mode: config says enabled false, and the learner app calls GET .../messages?after=id every 5 s instead
```

***

## 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**

```bash theme={null}
cd /var/www/vhosts/<domain>/httpdocs          # the folder that holds `artisan`
PHP=/opt/plesk/php/8.2/bin/php                 # Plesk's PHP 8.2, not the system `php`
$PHP -v                                        # must print 8.2 or newer
```

In Plesk, check that the **domain** runs PHP 8.2 too (Websites & Domains → the domain → **PHP Settings**). A domain on an older PHP fails on the new chatroom classes while the command line works.

**2. Get the code**

```bash theme={null}
git fetch origin
git checkout chat+ai        # or main, once the chatrooms are merged into it
git pull
git log --oneline -6        # should list "Add chatrooms with Reverb real-time events" and the slug commits after it
```

**3. Install the packages**

```bash theme={null}
composer install --no-dev --optimize-autoloader
# composer picks the wrong PHP? run its phar with Plesk's: $PHP /path/to/composer.phar install --no-dev --optimize-autoloader
# no composer at all? Plesk → the domain → PHP Composer → Install
$PHP -r 'require "vendor/autoload.php"; echo class_exists("Laravel\\Reverb\\ReverbServiceProvider") ? "reverb OK" : "reverb MISSING", PHP_EOL;'
```

The `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 `.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).

```env theme={null}
####### Chatrooms real time (Laravel Reverb) #######
# log = no WebSocket, the apps poll every 5 s. reverb = live, once Reverb runs (section 5).
BROADCAST_DRIVER=log
REVERB_APP_ID=<generated>
REVERB_APP_KEY=<generated>
REVERB_APP_SECRET=<generated>
# where the Reverb process listens (local only, nginx proxies to it)
REVERB_SERVER_HOST=127.0.0.1
REVERB_SERVER_PORT=8080
# where Laravel posts the events (the same local process)
REVERB_HOST=127.0.0.1
REVERB_PORT=8080
REVERB_SCHEME=http
# what GET /api/realtime/config tells the apps to open (filled in section 5)
REVERB_CLIENT_HOST=
REVERB_CLIENT_PORT=
REVERB_CLIENT_SCHEME=
# several Reverb processes behind a load balancer only (needs Redis, section 6)
REVERB_SCALING_ENABLED=false
```

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.

> [tab] On your computer (no SSH needed)
>
> In a macOS or Linux terminal, this prints the three lines ready to paste, with only lowercase letters and digits (what Reverb and the `.env` file need):
>
> ```bash theme={null}
> printf 'REVERB_APP_ID=%s\nREVERB_APP_KEY=%s\nREVERB_APP_SECRET=%s\n' "$(php -r 'echo random_int(100000, 999999);')" "$(openssl rand -hex 10)" "$(openssl rand -hex 10)"
> ```
>
> If PHP gives you trouble, this version uses only `openssl`:
>
> ```bash theme={null}
> printf 'REVERB_APP_ID=%s\nREVERB_APP_KEY=%s\nREVERB_APP_SECRET=%s\n' "$(( 0x$(openssl rand -hex 3) % 900000 + 100000 ))" "$(openssl rand -hex 10)" "$(openssl rand -hex 10)"
> ```
>
> 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.

> [tab] On the server (SSH)
>
> ```bash theme={null}
> PHP=/opt/plesk/php/8.2/bin/php                           # in the same session as the lines below
> $PHP -r 'echo random_int(100000, 999999), PHP_EOL;'     # REVERB_APP_ID
> $PHP -r 'echo bin2hex(random_bytes(10)), PHP_EOL;'      # REVERB_APP_KEY    (public: the apps receive it)
> $PHP -r 'echo bin2hex(random_bytes(10)), PHP_EOL;'      # REVERB_APP_SECRET (private: signs the subscriptions)
> ```

**Paste them without SSH.** Plesk → **Files** → open the backend folder → `.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).

> [accordion] Why the PHP command fails on the server
>
> Two common causes, and the error tells you which one:
>
> * **`-r: command not found`**: `$PHP` is empty. The line `PHP=/opt/plesk/php/8.2/bin/php` (step 1) has to run first **in the same SSH session**; a new session forgets it. Plain `php -r '…'` also works when the system `php` is recent enough.
> * **`No such file or directory`** for `/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/bash` instead of `/bin/bash (chrooted)`. Generating the keys on your computer avoids the problem altogether.

Leave the rest of `.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**

```bash theme={null}
$PHP artisan migrate --force
$PHP artisan migrate:status | grep -E "chatrooms_tables|classroom_slugs"   # both lines end with "Ran"
$PHP artisan tinker --execute='echo Schema::hasTable("chatroom_messages") ? "tables OK" : "TABLES MISSING";'
```

| Migration | What it does | Safe on a live database? |
| :- | :- | :- |
| `2026_10_20_000000_create_chatrooms_tables` | creates `chatrooms`, `chatroom_members`, `chatroom_messages`, `chatroom_attachments` ([diagram](https://theteam-13fc7f49.mintlify.site/api-reference/chatrooms.md#2-4-database-tables)) | yes, new tables only |
| `2026_10_20_100000_backfill_missing_classroom_slugs` | gives a slug to classrooms that still have none (rows older than the slug column) | yes, data only, other rows untouched |

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**

```bash theme={null}
$PHP artisan optimize:clear
$PHP artisan optimize
```

Then restart the domain's PHP so OPcache drops the old files: Plesk → the domain → **PHP Settings** → **Apply** (re-saving restarts the handler), or as root `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.

```text theme={null}
/opt/plesk/php/8.2/bin/php /var/www/vhosts/<domain>/httpdocs/artisan optimize:clear
```

**7. Allow 20 MB images**

* Plesk → the domain → **PHP Settings**: `upload_max_filesize = 25M` and `post_max_size = 25M`.
* Plesk → the domain → **Apache & nginx Settings** → *Additional nginx directives*:

```nginx theme={null}
client_max_body_size 25m;
```

Without them a large image answers `413` (nginx) or `422` "failed to upload" (PHP).

**8. Run the scheduler**

Plesk → **Scheduled Tasks** → *Add task* → *Run a command*, every minute (`* * * * *`), as the site's user:

```text theme={null}
/opt/plesk/php/8.2/bin/php /var/www/vhosts/<domain>/httpdocs/artisan schedule:run >> /dev/null 2>&1
```

It runs `chatrooms:prune-attachments` every night (images uploaded but never sent, older than 24 hours) and the existing notification clean-up.

**9. Check**

```bash theme={null}
$PHP artisan route:list --path=chatrooms      # 11 routes (DELETE api/chatrooms/{slug} since 04/11/2026)
$PHP artisan route:list --path=realtime       # GET api/realtime/config
$PHP artisan route:list --path=broadcasting   # GET|POST api/broadcasting/auth
$PHP artisan schedule:list | grep chatrooms   # 0 0 * * * php artisan chatrooms:prune-attachments
```

From your machine, with a token of any signed-in user (from `POST /api/auth/login`; use a test account, never paste a real password into a shared script):

```bash theme={null}
curl -s -H "Accept: application/json" -H "Authorization: Bearer <token>" https://<backend-host>/api/realtime/config
# {"enabled":false}   ← polling mode, as expected at this point
```

Then in the app: create a classroom, **Save & next**, **Create chatroom** on step 2, send a message. It shows up on the other side within about 5 seconds.

> [!NOTE]
>
> 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.

```bash theme={null}
# keys that appear more than once in .env
grep -E '^[A-Z_]+=' .env | cut -d= -f1 | sort | uniq -d
```

### 4.2 What each variable means

| Variable | Meaning | Server value | Developer machine |
| :- | :- | :- | :- |
| `BROADCAST_DRIVER` | `log` = no WebSocket (the apps poll), `reverb` = live | `log` until section 5 is done, then `reverb` | `reverb` while `reverb:start` runs |
| `REVERB_APP_ID` | the Reverb "app" id | 6 random digits | any |
| `REVERB_APP_KEY` | public key; `GET /api/realtime/config` sends it to the apps | 20 random characters | any |
| `REVERB_APP_SECRET` | private; signs channel subscriptions. Never in the app, never in a log | 20 random characters | any |
| `REVERB_SERVER_HOST` / `PORT` | where the Reverb process **listens** | `127.0.0.1` / `8080` | same |
| `REVERB_HOST` / `PORT` / `SCHEME` | where **Laravel posts** the events | `127.0.0.1` / `8080` / `http` | same |
| `REVERB_CLIENT_HOST` / `PORT` / `SCHEME` | what the **apps open**: `wss://host:port/app/key` | the public backend host / `443` / `https` | empty (the apps open `ws://127.0.0.1:8080`) |
| `REVERB_SCALING_ENABLED` | several Reverb processes sharing events through Redis | `false` | `false` |

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

| Key | Value the chatrooms need |
| :- | :- |
| `QUEUE_CONNECTION` | `sync` (keep) |
| `CACHE_DRIVER` | `file` is fine. `reverb:restart` signals the process through the cache, so Reverb must run as the site's user |
| `R2_*` | the existing Cloudflare R2 settings; chat images go to `files/all-chatrooms/<slug>/`, admin backups to `files/backups/conversations/<slug>/` (since 01/11/2026; older objects stay under `chatrooms/<slug>/` and `files/conversations/`). Check them with `GET /status/r2-check` on the backend host |
| `APP_DEBUG` | `false` in production. With `true` (debugging a test server) a failing chatroom call also returns `exception` and `debug` in its `500`, which names the cause |
| `LOG_CHANNEL` | the real error of a failing chatroom call is in `storage/logs/laravel.log` |

***

## 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**

```bash theme={null}
$PHP artisan reverb:start --debug
#   INFO  Starting server on 127.0.0.1:8080.
```

In a second SSH shell, send a test event (the prefix forces Reverb while `.env` still says `log`; it works when the config is not cached, otherwise do this check after the switch step):

```bash theme={null}
BROADCAST_DRIVER=reverb $PHP artisan tinker --execute='event(new App\Events\ChatroomUpdated(0, "ping", true)); echo "sent";'
```

The first shell logs a block for `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:

> [tab] supervisor
>
> ```bash theme={null}
> apt install supervisor            # Debian / Ubuntu (dnf install supervisor on AlmaLinux)
> nano /etc/supervisor/conf.d/reverb-test.conf
> ```
>
> ```ini theme={null}
> [program:reverb-test]
> command=/opt/plesk/php/8.2/bin/php /var/www/vhosts/<domain>/httpdocs/artisan reverb:start --host=127.0.0.1 --port=8080
> directory=/var/www/vhosts/<domain>/httpdocs
> user=<site system user>
> autostart=true
> autorestart=true
> startretries=10
> stopwaitsecs=10
> stdout_logfile=/var/www/vhosts/<domain>/logs/reverb.log
> stderr_logfile=/var/www/vhosts/<domain>/logs/reverb-error.log
> ```
>
> ```bash theme={null}
> supervisorctl reread
> supervisorctl update
> supervisorctl status reverb-test        # RUNNING
> ```

> [tab] systemd
>
> ```ini theme={null}
> # /etc/systemd/system/reverb-test.service
> [Unit]
> Description=Laravel Reverb (TEST)
> After=network.target
>
> [Service]
> User=<site system user>
> WorkingDirectory=/var/www/vhosts/<domain>/httpdocs
> ExecStart=/opt/plesk/php/8.2/bin/php artisan reverb:start --host=127.0.0.1 --port=8080
> Restart=always
> RestartSec=3
>
> [Install]
> WantedBy=multi-user.target
> ```
>
> ```bash theme={null}
> systemctl daemon-reload
> systemctl enable --now reverb-test
> systemctl status reverb-test
> ```

One program per environment. When two environments share a server, give each its own port (for example TEST `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 `/storage/` CORS block that lets the Flutter web app draw local-storage images (see [File Management & Storage](https://theteam-13fc7f49.mintlify.site/api-reference/file-management-and-storage-api.md)), 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:

```nginx theme={null}
# Chat images up to 20 MB (PHP: upload_max_filesize = 25M, post_max_size = 25M)
client_max_body_size 25m;

# Local-storage images readable by the Flutter web app (CanvasKit needs CORS on them)
location ^~ /storage/ {
    add_header Access-Control-Allow-Origin "*" always;
    try_files $uri =404;
}

# Chatrooms: wss:// on the site's certificate, forwarded to the local Reverb process
location ^~ /app/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_read_timeout 120s;
    proxy_send_timeout 120s;
}
```

* `^~` 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 plain `location /app/` works in practice too; `^~` is the safer form.
* `X-Forwarded-For` passes the browser's address on to Reverb, and `proxy_send_timeout` gives sends the same 120 s as reads.
* `8080` is `REVERB_SERVER_PORT`; use the environment's own port when several share a server.
* Plesk checks the syntax and reloads nginx when you press **OK**.

Check the proxy from your machine:

```bash theme={null}
curl -s -i -N --http1.1 --max-time 5 \
  -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
  "https://<backend-host>/app/<REVERB_APP_KEY>?protocol=7"
# 101 Switching Protocols + a pusher:connection_established line = nginx and Reverb both work
# Laravel's 404 page                                              = the /app/ block is missing or not applied
# 502 Bad Gateway                                                 = nginx forwards, but Reverb is not running
```

A plain `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 `.env`:

```env theme={null}
BROADCAST_DRIVER=reverb
REVERB_CLIENT_HOST=<backend-host>      # for example the TEST backend host, without https://
REVERB_CLIENT_PORT=443
REVERB_CLIENT_SCHEME=https
```

```bash theme={null}
$PHP artisan optimize:clear && $PHP artisan optimize
$PHP artisan reverb:restart                       # Reverb re-reads the same .env
curl -s -H "Accept: application/json" -H "Authorization: Bearer <token>" https://<backend-host>/api/realtime/config
# {"enabled":true,"driver":"reverb","key":"<the public key>","host":"<backend-host>","port":443,"scheme":"https","auth_endpoint":"/api/broadcasting/auth"}
```

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.

**After every later deploy** (Reverb runs the code it started with):

```bash theme={null}
git pull
composer install --no-dev --optimize-autoloader
$PHP artisan migrate --force
$PHP artisan optimize:clear && $PHP artisan optimize
$PHP artisan reverb:restart          # finishes its connections, exits, supervisor starts the new code
# + restart the domain's PHP (Plesk → PHP Settings → Apply)
```

**Switching back to polling**: `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`.

> [accordion] Only if you ever run several Reverb servers
>
> 1. Run a Redis server reachable by every Reverb machine (`apt install redis-server`, protected by a password and a firewall).
>
> 2. In every backend `.env`:
>
>    ```env theme={null}
>    REVERB_SCALING_ENABLED=true
>    REVERB_SCALING_CHANNEL=reverb
>    REDIS_HOST=<redis host>
>    REDIS_PORT=6379
>    REDIS_PASSWORD=<redis password>
>    ```
>
> 3. `optimize:clear`, `optimize`, then `reverb:restart` on every machine.
>
> Reverb talks to Redis with its own bundled client (`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

| | DEV | TEST | PROD |
| :- | :- | :- | :- |
| Backend host (`REVERB_CLIENT_HOST`) | `backend-dev.the-team.tn` | `backend-test.theteam.com.tn` | `service.elearning.theteam.com.tn` |
| Reverb keys | its own set | its own set | its own set |
| supervisor program | `reverb-dev` | `reverb-test` | `reverb-prod` |
| Reverb port | `8080`, or the next free one if the server hosts another environment | `8080` | `8080` |
| `BROADCAST_DRIVER` before section 5 | `log` | `log` | `log` |
| `APP_DEBUG` | `true` or `false` | `false` once debugging is over | `false` |

***

## 8. Developer machine

```bash theme={null}
cd LMS_Backend
# .env: the block of section 3 with any generated keys, BROADCAST_DRIVER=reverb, REVERB_CLIENT_* empty
php artisan migrate
php artisan serve --port=8020          # shell 1
php artisan reverb:start --debug       # shell 2
```

* The Flutter web app on `localhost:5000` with `Environment.localhost` in `lib/core/constants/constants.dart` connects to `ws://127.0.0.1:8080` directly (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 asks `GET /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**:

```bash theme={null}
cd LMS_Frontend
git checkout chat+ai && git pull
flutter pub get
dart run easy_localization:generate -S "assets/translations" -O "lib/core/translations"
dart run easy_localization:generate -S "assets/translations" -O "lib/core/translations" -o "local_keys.g.dart" -f keys
dart run build_runner build -d
flutter run -d chrome --web-hostname localhost --web-port 5000     # or the web deploy script for a server
```

`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

| Symptom | Cause | Fix |
| :- | :- | :- |
| Every chatroom call answers `500 "Something went wrong with the chatroom"`, classrooms work, the log says `Base table or view not found` | the migrations did not run on **this** database | section 3, step 5, on the server with Plesk's PHP |
| Same, the log says `ParseError` or `syntax error` | the domain runs a PHP older than 8.2 | Plesk → PHP Settings → 8.2 |
| Same, right after a pull, or old and new behaviour mixed | stale config / route cache or OPcache | `optimize:clear`, `optimize`, restart the domain's PHP |
| Step 2 **Create chatroom** → `422`, "A chatroom with the slug … already exists" | a chatroom with that slug exists already (left by a classroom deleted before 20/10/2026, or another company's) | on step 1 unlock the slug, change it, **Save & next**, then create the chatroom |
| `GET /api/realtime/config` → `{"enabled":false}` although you set `reverb` | `BROADCAST_DRIVER` twice in `.env`, cached config, or an empty `REVERB_APP_KEY` | one line per key (4.1), `optimize`, fill the keys |
| The app keeps polling, Reverb's log shows no connection | nginx `/app/` block missing or wrong port, or `REVERB_CLIENT_*` wrong | section 5, proxy step and its `curl` check |
| The WebSocket closes with code `4001` | the key the API sends is not the one Reverb started with | same `.env` for both, then `reverb:restart` |
| Messages save, the log shows `cURL error 7 … 127.0.0.1:8080` on each send | Reverb is down (the apps fall back to polling meanwhile) | `supervisorctl status`; or go back to `log` until it runs |
| `reverb:restart` does nothing | Reverb runs as another user than the site (no shared file cache) | run it as the site's user |
| Image upload → `413` or `422` "failed to upload" | upload limits | section 3, step 7 |
| Unsent images pile up in R2 `chatrooms/` | no scheduler | section 3, step 8 |
| `curl -X POST` answers an HTML **403 Forbidden**, then everything times out | Plesk firewall + fail2ban on a body-less POST | send `-d '{}'`; unban the IP in Plesk |
| `$PHP -r …` on the server → `-r: command not found` | `$PHP` was never set in this SSH session | run `PHP=/opt/plesk/php/8.2/bin/php` first, or generate the keys on your computer (section 3, step 4) |
| `/opt/plesk/php/8.2/bin/php: No such file or directory` over SSH | the SSH shell is chrooted | a Plesk admin sets SSH access to `/bin/bash`; meanwhile generate the keys on your computer and run `artisan` through Scheduled Tasks → **Run Now** (section 3, step 6) |

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

* `.env` stays on the server, outside git. This page and the repositories contain placeholders only.
* One Reverb key set per environment. `REVERB_APP_KEY` is public by design; `REVERB_APP_SECRET` never leaves the server.
* Reverb listens on `127.0.0.1` only; 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=false` on production.
* A key that was shared outside the server (chat, ticket, screenshot) is rotated: generate a new value, update `.env`, `optimize`, `reverb:restart`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.