Real-time updates in Laravel apps used to mean reaching for a third-party SaaS like Pusher or Ably that charges based on total concurrent connections. Laravel Reverb flips that model: it’s a self-hosted WebSocket server you run alongside your app, with no per-connection bill. This tutorial walks you through building a small voting app where everyone watching the page sees votes update live. You’ll start with a local DDEV environment, deploy to Upsun, and then scale the Reverb layer horizontally so a single project can handle far more concurrent WebSocket connections than one instance allows.Documentation Index
Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
Use this file to discover all available pages before exploring further.

What you’re building
A one-page Laravel app that lists 13 dinosaurs. Visitors vote for their favourite, and every connected browser sees the totals tick up in real time. Reverb handles the WebSocket fan-out, Inertia and Vue drive the UI, and MariaDB stores the counts. The deployed setup uses three containers:- A
dinosaursPHP app serving HTTP traffic - A
reverbPHP app running the WebSocket server - A
mariadbservice for persistence
.upsun/config.yaml. Adding a service later (you’ll add Redis at the end) is a few extra lines of YAML and a git push, no provisioning steps.
Prerequisites
Before you start, install:Local development with DDEV
Scaffold the project
Create the project directory and configure DDEV for Laravel:Terminal
Terminal
Create the Laravel project
Run the Laravel installer inside the DDEV container, targeting a temporary subdirectory:Terminal
| Prompt | Answer |
|---|---|
| Update Laravel installer? | No |
| Which starter kit? | Vue |
| Authentication provider? | No authentication scaffolding |
| Testing framework? | Pest |
| Install Laravel Boost? | No |
Run npm install and npm run build? | Yes |
Terminal
Terminal
Initialize Git
Terminal
Project structure
You’ll create or edit the following files:| File | Purpose |
|---|---|
.environment | Maps Upsun relationships to Laravel env vars |
.upsun config.yaml | Upsun infrastructure configuration |
app Events DinosaurVoted.php Http/Controllers DinosaurController.php Models Dinosaur.php | Model, controller, and broadcast event |
database/migrations 2026_04_16_190223_create_dinosaurs_table.php | Schema and seed data in one migration |
resources/js/pages Welcome.vue | The voting UI |
routes web.php | The two HTTP routes |
Enable broadcasting
Run the Laravel broadcaster installer. It installs the Reverb package, generates credentials in.env, and installs the frontend broadcasting package:
Terminal
| Prompt | Answer |
|---|---|
| Would you like to install Laravel Reverb? | Yes |
| Enable event broadcasting? | Yes |
| Install and build Node dependencies? | Yes |
.env and update these two values so the local frontend can reach Reverb through DDEV’s HTTPS proxy:
.env
.ddev/config.yaml so DDEV exposes the Reverb port and runs the Reverb daemon:
.ddev/config.yaml
Terminal
Application code
Routes
Replace the contents ofroutes/web.php:
routes/web.php
Model
Createapp/Models/Dinosaur.php:
app/Models/Dinosaur.php
Controller
Createapp/Http/Controllers/DinosaurController.php. The vote action increments the counter and dispatches a DinosaurVoted event that Reverb broadcasts to every connected client:
app/Http/Controllers/DinosaurController.php
Broadcast event
Createapp/Events/DinosaurVoted.php. It implements ShouldBroadcastNow so the event is sent on the public dinosaurs channel as soon as a vote lands:
app/Events/DinosaurVoted.php
Migration
Createdatabase/migrations/2026_04_16_190223_create_dinosaurs_table.php. The migration creates the table and seeds 13 dinosaurs in one step, so you don’t need a separate seeder:
database/migrations/2026_04_16_190223_create_dinosaurs_table.php
Frontend
Replaceresources/js/pages/Welcome.vue with the voting UI. The page uses useEchoPublic to subscribe to the dinosaurs channel and update vote counts as DinosaurVoted events arrive:
resources/js/pages/Welcome.vue
Run it locally
Apply the migration and build the frontend assets:Terminal
ddev launch and vote in two browser windows side by side. The counts update in real time, without a page refresh.
Upsun configuration
With the app working locally, you can now describe the production setup. Two files do the work:.environment maps Upsun-managed credentials to Laravel’s expected env vars, and .upsun/config.yaml defines the containers, services, and routes.
The .environment file
Upsun reads .environment at runtime to set environment variables for every container. It’s the right place to derive Laravel’s config from service relationships, which Upsun injects into the runtime as $MARIADB_HOST, $MARIADB_USERNAME, and so on. You never write credentials into your repo, and the same .environment works unchanged on every branch and preview environment.
Two things are worth calling out:
APP_KEYis derived fromPLATFORM_PROJECT_ENTROPY, a stable per-project secret that Upsun generates for you.- The public Reverb host is extracted from
PLATFORM_ROUTESat runtime withjq, so the frontend always points at the right URL across branches and preview environments.
.environment at the project root. Replace <YOUR_REVERB_APP_ID> and <YOUR_REVERB_APP_KEY> with the values the broadcaster installer generated in your local .env:
.environment
The .upsun/config.yaml file
Two applications share the same source tree: dinosaurs serves HTTP, and reverb runs the WebSocket server. Both connect to a shared MariaDB service. Running two apps from one repository is a built-in pattern on Upsun, so there’s no separate Reverb project to deploy, manage, or pay for on the side.
A few details worth flagging:
- Cache directories (
bootstrap/cache,.config) useinstancemounts, so each container gets its own writable copy. That matters once you scale. - The
optimize:clearstep lives inweb.commands.pre_start, not the deploy hook. The deploy hook only runs on one instance, butpre_startruns on every container at boot, which is what you want when scaling horizontally. - The
ws.{default}route for Reverb disables caching and request buffering, both of which would break long-lived WebSocket connections.
.upsun/config.yaml:
.upsun/config.yaml
Deploy to Upsun
Create the project
Terminal
| Prompt | Answer |
|---|---|
| Project title | Dinosaurs |
| Region | Pick the lowest-carbon region near you |
| Default branch | main |
| Switch the remote project for this repository to the new project? | Y |
| Are you sure you want to continue? | Y |
Add the Reverb secret
The Reverb app secret is sensitive, so it doesn’t belong in.environment. Store it as a project-level variable with the --sensitive flag, which hides it from logs and the UI:
Terminal
Push
Terminal
.upsun/config.yaml, builds both PHP apps, provisions MariaDB, wires the relationships, and issues a Let’s Encrypt certificate for the routes. There’s nothing to click in a console.
Once the deploy finishes, open the site:
Terminal
Scale Reverb horizontally
A single Reverb container has a ceiling on how many WebSocket connections it can hold open. To go beyond that ceiling, you need multiple Reverb instances coordinating through a shared backend, because each WebSocket connection is pinned to one container. When a vote arrives at one instance, every other instance has to be told so it can forward the update to the clients connected to it. Reverb solves this with Redis pub/sub. On Upsun, adding Redis to the stack is three lines of YAML: a service declaration and a relationship on each app. There’s no Redis instance to provision separately, no firewall rules to open, and the credentials show up in your runtime env automatically.Add Redis to .upsun/config.yaml
Add redis: to the relationships block of the dinosaurs app:
.upsun/config.yaml
reverb app too:
.upsun/config.yaml
.upsun/config.yaml
Wire Redis credentials into .environment
Append to the bottom of .environment:
.environment
REVERB_SCALING_ENABLED=true tells Reverb to use Redis pub/sub to fan events out across instances.
Deploy and scale
Commit and push:Terminal
Terminal
Where to go from here
- Reverb documentation for advanced options like private and presence channels.
- Upsun resources to size containers and tune the number of Reverb instances.
- Preview environments to test broadcast changes on a branch before promoting to production.