Skip to main content
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. Dinosaurs voting app with live vote counts updating across browsers

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 dinosaurs PHP app serving HTTP traffic
  • A reverb PHP app running the WebSocket server
  • A mariadb service for persistence
All three live in the same Upsun project and are described in a single .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
Add the Laravel installer to the DDEV container so you can scaffold the app from inside it:
Terminal

Create the Laravel project

Run the Laravel installer inside the DDEV container, targeting a temporary subdirectory:
Terminal
Answer the prompts as follows: Move the generated files to the project root and clean up:
Terminal
Restart DDEV and finish the Laravel setup:
Terminal
You should now see the default Laravel welcome page in your browser.

Initialize Git

Terminal

Project structure

You’ll create or edit the following files:

Enable broadcasting

Run the Laravel broadcaster installer. It installs the Reverb package, generates credentials in .env, and installs the frontend broadcasting package:
Terminal
Open the generated .env and update these two values so the local frontend can reach Reverb through DDEV’s HTTPS proxy:
.env
Append the following to .ddev/config.yaml so DDEV exposes the Reverb port and runs the Reverb daemon:
.ddev/config.yaml
Restart DDEV to apply the daemon config:
Terminal

Application code

Routes

Replace the contents of routes/web.php:
routes/web.php

Model

Create app/Models/Dinosaur.php:
app/Models/Dinosaur.php

Controller

Create app/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

Create app/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

Create database/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

Replace resources/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
Open the site with 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_KEY is derived from PLATFORM_PROJECT_ENTROPY, a stable per-project secret that Upsun generates for you.
  • The public Reverb host is extracted from PLATFORM_ROUTES at runtime with jq, so the frontend always points at the right URL across branches and preview environments.
Create .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) use instance mounts, so each container gets its own writable copy. That matters once you scale.
  • The optimize:clear step lives in web.commands.pre_start, not the deploy hook. The deploy hook only runs on one instance, but pre_start runs 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.
Create .upsun/config.yaml:
.upsun/config.yaml

Deploy to Upsun

Create the project

Terminal
Answer the prompts:

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
Replace <YOUR_REVERB_APP_SECRET> with the REVERB_APP_SECRET value from your local .env. Never commit it to the repository.

Push

Terminal
Upsun reads .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
Open the URL in two browsers and try voting. Votes propagate live across both windows, served by a single Reverb instance.

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
Add it to the reverb app too:
.upsun/config.yaml
Then declare the service:
.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
Scale the Reverb app to two instances. Upsun load-balances the WebSocket connections for you, so there’s no extra proxy or sticky-session configuration to manage:
Terminal
That’s it. The site is live, votes update in real time, and the WebSocket layer can grow horizontally as traffic does.

Where to go from here

Last modified on May 20, 2026