Skip to content
Scalpels home

Scalpels is officially launching on October 31, 2026. Special early adopter pricing is available now.

← All ideas and experiments

Experiment

Campfire on Laravel Cloud: what it actually took

· Ed Grosvenor

Short version: it works. Campfire is running on Laravel Cloud's early-access Ruby runtime, on managed Postgres, a managed Valkey cache, an S3-compatible bucket and a Resque worker. Two people can chat in a room in real time, attachments land in the bucket, search works, and everything survives a redeploy. Campfire's full test suite passes on Postgres: 461 runs, no failures.

The fork is public at artisan-build/campfire-experiment. It's a throwaway, built to find out what the minimum change looks like, not to run in production. Most of the porting was done by coding agents working from written briefs, with us checking their claims against the live app.

The guesses, scored

We guessed What happened
SQLite would have to go Right. Postgres, plus a rewrite of the FTS5 search onto Postgres full-text search
S3 would have a surprise in it Right, twice: a checksum clash on upload, and image variants that assume a disk
Managed Redis might object to something Right. The Valkey ACL refuses a command Action Cable sends on connect
Websockets were the real question Right. They don't get through, so we had to go around
Early access would have rough edges Right, and the biggest one was one we never guessed

Eighteen deploys, and the first seventeen failed

The Ruby build worked first time. The deploys did not. Every one failed with the same status, deploy.app.crashing, even though the logs showed Puma booting cleanly and answering health checks from inside the container.

We ruled things out one variable per deploy: port mismatches, the health-check path, the worker process, a leftover instance, hibernation. None of it mattered. The answer was in the dashboard's deploy log and nowhere else. The API and CLI only ever report the status string. Laravel Cloud's network is IPv6-only. Puma was listening on 0.0.0.0, the IPv4 wildcard, so nothing on Cloud's network could reach it.

The fix has two parts. The bind address becomes configurable, with the upstream default left alone:

# config/puma.rb
# Laravel Cloud's network is IPv6-only, so production sets
# PUMA_BIND_HOST=[::]; the default stays IPv4 to match upstream.
PORT=ENV.fetch("PORT", 3000)
bind "tcp://#{ENV.fetch("PUMA_BIND_HOST", "0.0.0.0")}:#{PORT}"

And the start script has to launch Puma directly, because rails server passes its own host and port and quietly overrides the config file:

# bin/start-app
exec bundle exec puma -C config/puma.rb

Deploy eighteen went green. The lesson we took away: when Cloud says crashing, read the dashboard deploy log before you try anything else.

The smaller fixes

Don't disable SSL. Campfire has a DISABLE_SSL setting for running behind a proxy that terminates TLS, which sounds like exactly Cloud's setup. Setting it made Rails think every request was plain HTTP while the browser was sending an HTTPS origin, so every form, starting with the first-run wizard, failed its CSRF check. Cloud terminates TLS and forwards the right headers, so leave it unset.

Valkey won't let you name your connection. Cloud's managed Valkey locks down what the app's user can do, and CLIENT SETNAME is not allowed. Action Cable names its Redis connection, so every message 500'd. The fix is a new initializer, not an edit to upstream code:

# config/initializers/action_cable_redis.rb
ActionCable::SubscriptionAdapter::Redis.redis_connector = ->(config) do
  ::Redis.new(config.except(:adapter, :channel_prefix, :id))
end

One checksum at a time. Recent versions of the AWS SDK add a CRC32 checksum to every upload, on top of the MD5 that Active Storage already sends. Cloud's S3-compatible storage rejects a request with both. Two lines in config/storage.yml fix it:

cloud:
  service: S3
  # ...
  request_checksum_calculation: when_required
  response_checksum_validation: when_required

Thumbnails assumed a disk. Uploads worked, but image variants (avatars and previews) 500'd, because the code asks for a local file path that an S3 service doesn't have. There is already an open pull request upstream that fixes it. We cherry-picked it rather than writing our own. Campfire is deliberately a single-server app, so we don't expect S3 fixes to be a priority upstream.

Realtime, the hard way

Our biggest question got a clear answer: Action Cable websockets do not get through Cloud's edge. The upgrade request to /cable comes back as a 520, because Cloud's web proxy doesn't forward the Upgrade header to the app. Action Cable itself was healthy; the connection just never reached it.

Cloud does have managed websockets. They are Reverb, which speaks the Pusher protocol. So instead of asking Action Cable to get through the proxy, we taught it to publish through Reverb:

  • A new Action Cable subscription adapter publishes every broadcast to Reverb's HTTP API. None of Campfire's existing broadcasts had to change.
  • In the browser, a small shim swaps Action Cable's consumer for pusher-js, so the existing channel code keeps working.
  • A channel auth endpoint reuses Campfire's own room permissions, so someone outside a room gets a 403 for its channel.

The cost was 11 lines changed across 4 upstream files, plus 17 new files. New files almost never conflict with upstream changes, so this is a much cheaper diff than it sounds.

Reverb on a Rails app had rough edges of its own. Cloud won't attach a Reverb application to a Rails environment yet, so the connection details are set by hand. A new Reverb app allows no origins until you say otherwise. And Reverb caps a message at 10,000 bytes, while a Campfire message broadcast is about 11,400, so we compress the payload before sending it.

Where it stands

Campfire is live on the Reverb branch, with realtime, attachments, search and a Resque worker all working. We're taking everything above to the Laravel Cloud team as early-access feedback. The two changes that would make most of the workarounds unnecessary are forwarding websocket upgrades to Rails apps, and letting a Rails environment attach Reverb the way a Laravel one can.

The bigger decision is still open. Keeping the diff small was the goal for the experiment. Some of what Cloud needs, starting with the S3 fixes, is unlikely to land upstream, which points toward a proper fork maintained for Cloud: it would watch upstream for patches and apply them deliberately, rather than trying to stay a thin layer on top. We will decide that after the conversation with Laravel, and a one-click Campfire install through Scalpels comes back on the table once we have.

The idea behind this experiment

  1. Idea

    Can Campfire run on Laravel Cloud?

    We just got early access to Ruby on Laravel Cloud, and 37signals open-sourced Campfire. It was built to run on one box you own, which is about as far from a managed cloud as you can get. How much has to change?

    Read the idea

Support the Lab

The Lab runs on our supporters

Every idea here gets tried for real: deployed, broken, fixed and written up. Right now that is one person doing the work between everything else, and supporters are what buy the time. As support grows, it will also pay freelancers to run experiments of their own, so more of these ideas get tested, sooner.

Our supporters

  • Sputnik Intelligence

    Spain

    We transcribe the podcasts and read the newsletters, and make them searchable. Set up alerts, or hand it all to your AI agent over MCP or API.

    Supporter Visit
    Visit Sputnik Intelligence