Skip to content

feat: run a Laravel app's Reverb server for its site - #81

Open
sineld wants to merge 12 commits into
flock-php:mainfrom
sineld:feat/reverb
Open

sineld wants to merge 12 commits into
flock-php:mainfrom
sineld:feat/reverb

Conversation

@sineld

@sineld sineld commented Oct 31, 2026 •

Copy link
Copy Markdown

Fixes #50

Replaces #21, which closed when develop was removed. Same commits, merged with main; nothing else changed.

This lets Flock run a Laravel app's Reverb server for its site. Browsers reach it at the site's own address, wss://app.test/app, through the web server, so there's no port to open and no separate certificate.

flock reverb on [site]        run it
flock reverb [site]           its state, and the .env lines the app needs
flock reverb restart [site]   e.g. to load changed code
flock reverb off [site]
app's Reverb server is running, log: `flock logs reverb-app`
in the app's .env:
  REVERB_HOST=app.test
  REVERB_PORT=443
  REVERB_SCHEME=https

install:broadcasting --reverb writes VITE_REVERB_* as references to these variables, so the Echo client follows them too.

What it does

  • A site setting, like FrankenPHP:

    • Site.Reverb and Site.ReverbPort are new fields.
    • The port comes from the same allocator as FrankenPHP's ports, from 21000 up, and is kept while Reverb is off.
    • UsedPorts names it reverb-<site>.
    • A site's FrankenPHP and Reverb ports never collide, even when both are switched on before either is saved.
  • The process:

    • The daemon supervises php artisan reverb:start --host=127.0.0.1 --port=<port> --hostname=127.0.0.1 in the project, with the site's PHP version and the same environment as its pool (dumps and capture included).
    • --hostname stops Reverb from serving TLS itself: given the site's name, it does so whenever Herd or Valet has a certificate for that name.
    • Like FrankenPHP, the reload waits for the port after a (re)start.
    • A project without laravel/reverb is reported as unavailable, with the install:broadcasting --reverb hint.
  • The web server: one handle @reverb block, placed before the site's other handlers (Xdebug's included), with this matcher:

    @reverb {
        path /app/* /apps/*
        expression `{http.request.header.Upgrade} != "" || {query.protocol} != "" || {query.auth_signature} != ""`
    }
    
    • WebSocket connections: matched by the Upgrade header. ?protocol= covers WebSockets over HTTP/2, which have no Upgrade header; pusher-js always sends it.
    • Broadcasts: the signed Pusher HTTP API (/apps/<id>/events) that Laravel broadcasts through.
    • Everything else on those paths stays the app's. A Filament panel at /app, for example, keeps working.
    • Laravel's PHP already trusts Flock's CA (curl.cainfo), so broadcasts to https://app.test work out of the box.
  • Restart:

    • RestartSite (POST /v1/sites/{name}/restart) now restarts a site's FrankenPHP and Reverb processes, using the counter FrankenPHP already had.
    • I preferred this to a second endpoint. flock frankenphp restart therefore also restarts Reverb, and Echo reconnects.
  • API: PATCH /v1/sites/{name} takes "reverb": true|false. SiteStatus.reverb carries the state, port, .env lines, problem and log name.

  • CLI: flock reverb as above. flock sites marks the site with + Reverb.

  • MCP:

    • update_site gets reverb_on and reverb_off.
    • site_info shows reverb_state and reverb_env.
    • The server instructions say not to run reverb:start when Flock runs it.

Not in this PR

  • The macOS and Windows apps: I can't build them here. They ignore the new reverb field, so nothing changes for them. Their site panes could get a Reverb toggle next to FrankenPHP, with the state, the .env lines and a restart button. I'm happy to do that in a follow-up, or leave it to you if you'd rather shape the UI yourself.
  • Writing .env: Flock only shows the lines. It doesn't touch the project's files.
  • A crash after start: if Reverb crashes after it started, the status says so at the next reload, as FrankenPHP's does.

Testing

  • Unit tests:
    • Port allocation (stable, kept while off, no clash with the site's FrankenPHP port).
    • reverbSite: missing Reverb, the command, its environment and the restart counter, and a port conflict.
    • PATCH on and off, and the refusal for a project without Reverb.
    • The Caddyfile places @reverb before Xdebug's block and adapts.
  • A real Caddy in front of a stand-in Reverb:
    • A WebSocket handshake over HTTPS reaches it and echoes through.
    • A signed broadcast and a ?protocol= request over HTTP/2 reach it.
    • /app/dashboard and /apps/list still go to PHP.
  • End to end: with Laravel 13.37 and Reverb from install:broadcasting --reverb, the process started as Flock starts it, behind this Caddyfile:
    • A Pusher-protocol client connected to wss://chat.test/app/<key> and subscribed to a channel.
    • A broadcast signed as Laravel's Pusher client signs it went over HTTP/2 to https://chat.test/apps/<id>/events and reached the WebSocket.
  • Commands, on the branch merged with main:
    • go build ./..., go vet ./... and go test ./... pass on Linux. go vet also passes for darwin and windows.
    • go test -race passes for the touched packages except internal/web, where TestServesAShareThroughCaddy reports a race. It does the same on main without this PR (-count=3), so it isn't from Reverb.

Docs

  • docs/php.md gets a Laravel Reverb section after FrankenPHP: what goes to Reverb, the .env lines, rebuilding the front end, restarting, the log, and the commands.
  • docs/cli.md lists the flock reverb commands. The flock frankenphp restart row now says that it restarts Reverb too.
  • docs/ai.md adds Reverb to site_info and update_site.
  • docs/introduction.md mentions Reverb next to FrankenPHP.
  • The app view says Reverb isn't in the app yet, the way postgres.md does for creating a database.

@onairmarc
onairmarc requested a review from lukas-frey November 2, 2026 02:08
sineld and others added 6 commits November 12, 2026 18:45
Flock runs `php artisan reverb:start` for a site on a loopback port, and the web server sends the site's WebSocket connections and broadcasts under /app and /apps there, so browsers reach it at wss://<site>.test/app.
Each has its own restart counter, so restarting Octane's workers no longer drops Reverb's connections. `flock reverb restart`, the API and the MCP tool's restart_reverb restart only Reverb.
… app

WebSockets over HTTP/2 are matched by ?protocol= together with ?client=, which pusher-js always sends.
The apps use reverb_available to offer the Reverb switch, as octane_available does for worker mode.
@lukas-frey

Copy link
Copy Markdown
Member

Thanks a lot for this, Sinan! We wanted to get Reverb in, so we finished it on your branch:

  • Rebased onto main (on top of Feature/node sites proxy commands #151). Your commits are squashed into one, still under your name.
  • Reverb and FrankenPHP restart separately now, so restarting Octane doesn't drop Reverb's connections. flock reverb restart and the MCP tool's new restart_reverb only restart Reverb.
  • ?protocol= only goes to Reverb together with ?client=, so an app's own ?protocol= filter under /app stays the app's.
  • The site status has reverb_available, and all three apps have a Reverb section in Sites: the switch, its state, the .env lines with Copy, Restart and its output.
  • Docs updated to match.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Reverb startup lacks a root-user guard, allowing project-owned PHP to execute with root privileges.

5 open issues
What changed in this PR

Adds per-site Laravel Reverb management to Flock, addressing #50. WebSocket connections and broadcasts use the site's existing web address without a separate public port or certificate.

Changes:

  • Adds Reverb configuration, port allocation, process supervision, and Caddy routing.
  • Exposes Reverb controls and independent restarts through the CLI, API, MCP, and native apps.
  • Adds backend and client tests, plus setup and usage documentation.
File Description
windows/​Flock.Tray/​ViewModels/​SitesViewModel.cs Tracks Reverb changes and diagnostics.
windows/​Flock.Tray/​Settings/​SitesPane.xaml.cs Includes the Reverb settings section.
windows/​Flock.Tray/​Settings/​SitesPane.Reverb.xaml.cs Adds Reverb controls, status, and output.
windows/​Flock.Tray/​Features/​Sites/​SitesService.cs Adds the Reverb restart operation.
windows/​Flock.Tray.Tests/​SitesWorkflowTests.cs Tests Reverb diagnostics in site summaries.
windows/​Flock.Kit/​Settings.cs Adds the Reverb setting to site updates.
windows/​Flock.Kit/​Models.cs Models Reverb availability and status.
windows/​Flock.Kit/​FlockClient.cs Adds restart requests and operation timeouts.
windows/​Flock.Kit.Tests/​DecodingTests.cs Tests Reverb status decoding.
windows/​Flock.Kit.Tests/​ClientTests.cs Tests Reverb requests and timeouts.
macos/​Tests/​FlockKitTests/​DecodingTests.swift Tests Reverb decoding and compatibility.
macos/​Tests/​FlockKitTests/​ClientTests.swift Tests Reverb API requests.
macos/​Sources/​FlockUI/​Settings/​SitesPane.swift Adds Reverb controls and diagnostics.
macos/​Sources/​FlockKit/​Settings.swift Adds the Reverb update field.
macos/​Sources/​FlockKit/​Models.swift Models Reverb status and availability.
macos/​Sources/​FlockKit/​Client.swift Adds the dedicated restart request.
linux/​src/​panes/​sites.rs Adds Reverb controls, diagnostics, and output.
linux/​src/​api.rs Adds Reverb models, requests, and decoding tests.
internal/​web/​web.go Routes Reverb traffic before other handlers.
internal/​web/​web_test.go Tests generated routing and handler order.
internal/​web/​serve_test.go Tests WebSocket forwarding and routing boundaries.
internal/​reverb/​reverb.go Defines installation detection, process specs, and environment guidance.
internal/​mcpserver/​tools_write.go Adds Reverb management actions.
internal/​mcpserver/​tools_read.go Exposes Reverb state and environment lines.
internal/​mcpserver/​server.go Extends the daemon interface and server instructions.
internal/​mcpserver/​mcpserver_test.go Extends the fake daemon for Reverb restarts.
internal/​daemon/​reverb.go Prepares Reverb processes and handles restarts.
internal/​daemon/​reverb_test.go Tests startup preparation, conflicts, and PHP release selection.
internal/​daemon/​frankenphp.go Clarifies independent Reverb restarts in a comment.
internal/​daemon/​daemon.go Integrates Reverb into reloads, status, and PHP release handling.
internal/​daemon/​daemon_test.go Tests blocked Reverb status when PHP is missing.
internal/​config/​config.go Adds Reverb settings and shared port allocation.
internal/​config/​config_test.go Tests port stability and collision avoidance.
internal/​cli/​status.go Marks Reverb-enabled sites in listings.
internal/​cli/​root.go Registers the Reverb command.
internal/​cli/​reverb.go Adds enable, disable, restart, and status commands.
internal/​cli/​reverb_test.go Tests status errors when the daemon is stopped.
internal/​api/​frankenphpapi.go Adds the dedicated Reverb restart endpoint and client method.
internal/​api/​configapi.go Handles Reverb site updates and installation validation.
internal/​api/​api.go Adds Reverb status contracts and extends reload timeout.
internal/​api/​api_test.go Tests enabling, disabling, and installation refusal.
docs/​php.md Documents Reverb setup, controls, and routing.
docs/​introduction.md Introduces managed Reverb support.
docs/​cli.md Lists Reverb commands.
docs/​ai.md Documents Reverb capabilities in MCP tools.

Review effort: Balanced

Comment thread internal/daemon/reverb.go
return nil, st
}

if !reverb.Installed(site.Path) {
Comment thread internal/cli/reverb.go Outdated
return errors.New("Laravel Reverb isn't installed in " + site.Path + " — run `php artisan install:broadcasting --reverb` first")
}
s.cfg.SetReverb(&site, on)
s.cfg.Upsert(site)
Comment thread internal/daemon/daemon.go
NodeShadowed: nodeVersion.Shadowed, Domains: []api.SiteDomain{},
}
status.Server, status.Worker, status.OctaneAvailable = config.ServerFastCGI, site.Worker, frankenphp.HasOctane(site.Path)
status.ReverbAvailable = reverb.Installed(site.Path)
Comment thread internal/daemon/daemon.go Outdated
Comment on lines +1258 to +1259
if site.Reverb && (minor == "" || project.PHPFor(cfg, &site, site.Path).Version == minor) {
reverbs[config.ReverbPortID(site.Name)] = true
Comment thread internal/daemon/reverb.go Outdated
return fmt.Errorf("Reverb is off for %s", name)
}
d.mu.Lock()
d.restarts[config.ReverbPortID(name)]++
Comment on lines +30 to +49
if (reverb is null)
{
section.Children.Add(Ui.Caption(site.ReverbAvailable
? $"Flock runs php artisan reverb:start for the site. Browsers reach it at {scheme}://{site.Domain}/app, with no port or certificate of its own."
: "Install Laravel Reverb in the project (php artisan install:broadcasting --reverb) to run it.", Ui.TertiaryText));
return section;
}

switch (reverb.State)
{
case Status.ReverbState.Running:
section.Children.Add(ReverbState(Ui.Tone.Success, $"Running at {scheme}://{site.Domain}/app"));
break;
case Status.ReverbState.Failed:
section.Children.Add(ReverbState(Ui.Tone.Critical, reverb.Problem.Length > 0 ? reverb.Problem : "Failed"));
break;
default:
section.Children.Add(Warning(reverb.Problem.Length > 0 ? reverb.Problem : "Reverb can't run."));
break;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All UI element creation should be done via XAML. We can conditionally modify them, but we shouldn't be adding new children in cs files.

Comment on lines +561 to +564
if (!site.IsNode)
{
panel.Children.Add(Reverb(site, row));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XAML

Comment on lines +389 to +395
private static bool ReverbEqual(Status.SiteReverb? previous, Status.SiteReverb? current)
{
return previous is null && current is null || previous is not null && current is not null &&
previous.State == current.State && previous.Port == current.Port && previous.Env.SequenceEqual(current.Env) &&
previous.Problem == current.Problem && previous.Log == current.Log;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatting can be improved here. Start each new condition on a new line with the && or || at the beginning of the line.

A worktree uses its parent's settings, so `flock reverb on` and `off` refused nothing and reported success.
The daemon doesn't run a project's Reverb server with root's rights, notices Reverb being installed, releases a Reverb server by the PHP binary it was started with, and keeps Reverb's restart counters apart from the sites'.
The site, worktree and proxy details and the pane's dialogs are XAML views bound to view models, instead of controls built in code.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Run a Laravel app's Reverb server for its site

4 participants