Roundhouse

Campfire — the app that pushes, not just responds

ONCE Campfire is Basecamp's chat app: a small, MIT-licensed Rails application you can read whole. It's here because of what it does after the response — seven Action Cable channels, model-side broadcasts, turbo_stream templates. Blog, Lobsters, and Mastodon are all request-in, HTML-out. Campfire is the axis none of them test.

Status: it runs — "two tabs, one room, one live message" is walked: an emitted binary serves a signed-in room, and a posted message fans out over the cable to the other tab. The write-up has the walk and what is not in the box; the download is below.

Run it

With Docker and nothing else. The archive is the program after Spinel has compiled it to C — generated code, the runtime as source, a Makefile — with a two-stage Dockerfile that builds the one and ships the other. About 5 MB down, about a minute to compile, and a 160 MB image whose only extras over debian:trixie-slim are the SQLite and jemalloc runtime libraries.

curl -sL https://rubys.github.io/roundhouse/campfire/docker.tgz | tar xz
cd campfire-docker
docker build -t campfire .
docker run --rm -p 3000:3000 -v campfire-data:/app/storage campfire

Then open http://localhost:3000/: campfire's own first-run page creates the account, the first user, and the first room. The archive's README covers ports, worker counts, and building without Docker.

Prefer to read the emitted Ruby? The source archive is the same program before compilation — the transpiled app, its tests and browser suite — and needs a Spinel checkout, a C compiler and Node to build. Its README is the recipe.

Explore its types (IDE) Transpile it (Playground) Benchmark results Conformance (their test suite) Conformance, compiled

Why Campfire

Feature-dense where Roundhouse is thin, and small enough to hold in your head: roughly 3,700 lines of application Ruby across seventeen models, forty-odd controllers, and eighty views. That combination is the point. Mastodon proves the analysis scales; Campfire pushes it sideways into constructs a read-heavy app never reaches for — broadcast_append_to and friends firing from model callbacks, turbo_stream_from in views, channels authorizing their own streams, a .turbo_stream.erb response picked by the Accept header.

It is also the sharpest version of the deployment argument. Campfire ships as a Docker image that wants Ruby, Redis, and a job runner beside it. The thing Roundhouse is aiming at is one static binary that holds the WebSockets itself — same app, same behavior, none of the runtime around it. That's a claim worth being held to, which is why it's a milestone here rather than a bullet point.

What works today

Open it in the IDE: the whole app ingests, and hover types, completion, request traceroute, and the coverage ledger answer across it — client-side, no app boot, no database. Open it in the Playground and it transpiles to any target, emitting what it can and leaving a diagnostic on every construct not yet modeled.

The unmodeled constructs are the interesting part, and they are on screen rather than in a footnote: each one is a diagnostic on its exact line. That ledger is the worklist — the same bar the blog already meets perfectly, Lobsters is close to, and Mastodon is climbing toward. Serving a live room is the next rung, not a finished claim.

A diagnostic says a construct wasn't modeled; it doesn't say whether what was emitted behaves. Campfire's own test suite answers that, and the conformance page publishes the result: every test file the app declares, run against a Roundhouse emit of it, with the failures clustered into a ranked list of what each one is waiting on. Two things make that number worth reading. The app is pinned to a fixed commit, so it is rebuilt on every push to this repo and any movement belongs to a Roundhouse change rather than to upstream drift. And the page lists what the harness stubs in to get the suite running at all — a pass rate propped up by the harness is not a pass rate about the emit, so the props are on the page beside it.

The same suite runs a second way, and the difference is the interesting part. On the compiled conformance page each test file is built to its own native binary by the Spinel AOT compiler and that binary is run — same emitted source, different runtime — so a gap between the two pages belongs to the compiler rather than to the app or to the emit. It also reports something the interpreted lane cannot: a file that never linked. Under CRuby a test file passes or fails; compiled, it can also fail to build, and its tests are then charged to a compile error without anything behind that error having been observed at all. Those two questions — does it compile, does it behave — are what the compiled page keeps in separate columns.

Further reading

Source: basecamp/once-campfire (MIT), pinned to a fixed commit; the shipped bundle embeds its LICENSE and that commit.