We compiled Deccan Queen on Rails’ (DQOR) Campfire fork into a Linux AMD64 executable with Roundhouse and Spinel. The useful work was making the compiled application preserve Rails behavior: identity handoff, persistent sessions, shared room forms and packaged assets.
This article describes the release verified on October 4, 2026 at 04:16 UTC. Its deployment receipt records the executable checksum and source revisions. Later local experiments are separate from that deployment. The talk-channel additions below have their own published prerelease and local test evidence; neither establishes a later production deployment.
What Roundhouse and Spinel compile
Sam Ruby’s Roundhouse lowers Campfire’s controllers, models and views into application Ruby, framework-runtime Ruby and inferred RBS signatures. Spinel compiles that Ruby to C. Clang then builds the native executable. The service uses Roundhouse’s framework runtime; it does not boot the Rails gems in the production container. Turbo, Stimulus and the editor still run in the browser. This is a compiled Campfire deployment, not evidence that the full PostgreSQL-backed DQOR application compiles or works natively.
The archive builder at be39e428 keeps those stages explicit. This emission excerpt uses the builder’s own $APP and $TREE paths; it is not a standalone setup command:
(cd "$REPO_ROOT" && cargo run --release --quiet --bin roundhouse -- \
--target spinel "$APP" -o "$TREE") 2>"$WORK/emit.log" || {
tail -30 "$WORK/emit.log" >&2
fail "transpile failed"
}There is no --allow-unsupported. An unsupported construct fails emission. The builder renames the generated entry point to campfire, runs bundle install and make assets, then invokes spin pack. Packing asks Spinel for generated C and its build ingredients, and includes runtime and native-package sources. Missing native-package warnings fail the archive build.
The two archives serve different purposes. spinel.tgz retains the emitted application and tests. docker.tgz contains the C build context and the files the executable reads at runtime. A recipient can build that context with Docker without installing Ruby or Spinel there. Ruby and Bundler are still build-time dependencies for generating the original assets.
The Docker recipe uses Debian trixie and Clang for Linux AMD64. Its runtime stage carries SQLite, jemalloc, libvips, OpenSSL, FFmpeg and CA certificates. The executable is dynamically linked, so distributing the ELF alone does not provide a working installation. The Debian base tag is not pinned to an image digest; source revisions and checksums identify this release, without promising byte-for-byte rebuilds from future package repositories.
Compatibility fixes that source tests could not establish
Rails source tests exercise Rails. Native integration tests exercise the generated runtime. Passing one cannot substitute for the other.
Our identity callback originally used the SQL-string overload of find_by. The native API did not accept that overload. The callback at Campfire 46838319 uses the supported relation API instead:
user = User.where("lower(email_address) = ?", email).firstThis retains the case-insensitive lookup and placeholder-style source API. Roundhouse’s native relation implementation substitutes placeholder values through the adapter’s escaping operation. Describing it as a SQLite prepared-statement binding would be inaccurate.
Two compiler corrections mattered beyond that callback:
| Failure | Correction and regression |
|---|---|
A shared room form submitted an open or closed room to the base /rooms route. |
The emitter derives the collection or member route from the stored STI subtype, even when the record is represented as the base class. Explicit form URLs retain precedence. See form_with_sti_routes.rs. |
Qualifying a template’s bare private helper call as Helper.private_method broke Ruby visibility rules. |
The emitter generates collision-safe public internal entry points for template calls while preserving the original helpers’ private or protected visibility. See the emitter and emit_and_run.rs. |
The helper regression runs emitted Ruby through CRuby. Native browser checks remain a separate layer. We also made the message edit form submit PATCH explicitly and the welcome controller render :show explicitly. Rails inferred these details; the pinned compiled runtime needed them expressed in source.
DQOR verifies identity; Campfire starts its own session
Google OAuth and email-link authentication run in DQOR. Campfire receives a short-lived identity grant from that application. This bridge reuses DQOR’s verified identity without sharing its session database or forwarding Google access and ID tokens to Campfire.
Campfire creates a random state and retains it, together with invitation context, in signed cookies with a ten-minute lifetime. On HTTPS those cookies use Secure, HttpOnly and SameSite=Lax. The invitation stays in Campfire’s cookie; it is not sent to DQOR.
DQOR reuses a verified main-site session or asks the person to authenticate with Google or an email link. The continuation change in PR #205 preserves the return to chat. DQOR issues a 60-second grant and stores its SHA-256 code digest.
This method is from DQOR 585f1e51, ChatLoginGrant. It belongs inside that model:
def self.redeem(code:, state:)
return unless CODE_FORMAT.match?(code.to_s) && STATE_FORMAT.match?(state.to_s)
transaction do
grant = lock.find_by(code_digest: Digest::SHA256.hexdigest(code))
if grant && grant.expires_at.future? && ActiveSupport::SecurityUtils.secure_compare(grant.state, state)
allowed = allowed_identity?(grant.email)
payload = { verified: true, email: grant.email, name: grant.name }
grant.destroy!
payload if allowed
end
end
endThe transaction locks the grant, checks expiry and state, rechecks DQOR’s eligibility policy, and consumes a valid matching grant even when eligibility now denies the identity. A second redemption cannot reuse that deleted row.
Campfire independently checks its signed state, redeems the code over HTTPS, and accepts only a verified response with a valid email. An existing user must be active and human; a new user needs the retained valid invitation. Configured organizer and speaker lists can promote an administrator or add Speaker Lounge membership on login. They do not establish bidirectional role synchronization or shared logout.
With GOOGLE_LOGIN_ENABLED=true, DQOR owns sign-in and account recovery. Campfire disables local password login and registration and prevents profile changes from replacing the verified email or password. The original local account flow remains available when delegation is disabled. Broader cross-application SSO is outside this released bridge.
Persistent cookies also need persistent server state
The native cookie proxy implemented permanent as an identity operation. The Rails call cookies.signed.permanent therefore left the native session cookie without an explicit browser expiry. Refreshing a tab worked; closing and reopening a persistent browser profile lost that session cookie.
We supplied the expiry in Campfire’s shared authentication writer. This is a formatted excerpt from set_authentication_cookie, where session is the authenticated Campfire session:
cookies.signed.permanent[:session_token] = {
value: session.token,
expires: 20.years.from_now,
httponly: true,
same_site: :lax,
secure: request.ssl?
}The native regression performs repeated refreshes, closes and reopens the same real browser profile without importing browser state, checks cookie protections, and signs out. The release’s validation records desktop Chromium and mobile-emulated Chromium and WebKit. Physical phones are outside that coverage. Twenty years is a requested, renewed cookie lifetime, not a guarantee of twenty years of access; browser caps and server-side session state still apply.
Container replacement is a separate persistence problem. Roundhouse’s runtime keeps its generated signing key in /app/storage/secret_key_base when no explicit key is supplied. That key, SQLite session records and uploaded files must survive replacement together. Changing the key invalidates signed cookies even if the browser retains them; losing the database loses the sessions they identify. The deployment mounts persistent storage and runs the application as UID/GID 1000. Restart behavior therefore depends on preserving storage, the signing key and permissions, not just rebuilding the executable.
Theme assets belong in the native release
We reused DQOR’s original Pune and railway artwork from de21ebff, retaining its MIT license and a source-to-asset hash map. The source file named hero-bg.jpg is PNG-encoded. Its WebP derivative keeps the 1536 × 1024 dimensions and reduces the file from 4,767,292 to 619,522 bytes. The sharper logo is a later generated presentation derivative, documented separately from the original badge in the theme’s asset mapping.
The theme uses Campfire’s existing CSS variables and operating-system light/dark preference. Artwork stays on entry surfaces; chat messages retain plain, opaque backgrounds. Here is the actual light-mode background rule from the pinned stylesheet:
.dq-entry #main-content {
background-image: linear-gradient(hsl(40 30% 96% / 0.85), hsl(40 30% 96% / 0.94)), url("deccan-queen/hero-bg.webp");
background-position: center;
background-size: cover;
min-block-size: 100dvh;
padding-block-end: 2rem;
}The native asset builder supplies the /assets files this relative URL resolves against. The stylesheet is explicitly linked after the base styles and before account custom styles. Compiling controller code without shipping those matching assets would leave the release incomplete; the Docker archive also copies source-path images read directly by controllers.
Reduced motion needs more care than disabling every animation. Campfire removes flash notices on animationend. The theme suppresses decorative movement but retains a three-second opacity-only notice animation, so notices stay readable and their existing cleanup still runs. This dated production theme adds no application gem, JavaScript package or remote font service.
A compiled talk-channel pilot captures questions and votes
The separately published redesign prerelease, Campfire 8c871c3, adds questions and voting to an existing room mapped to a TalkSlot. It is default off: TALK_CHANNELS_ENABLED=true enables the pilot. The release is an artifact, not proof that projection or conference channels are active in production. Its synthetic local account tests do not revalidate the deployed DQOR identity bridge.
Submitting /ask through the ordinary message endpoint stores both the chat message and its question capture in one transaction. A unique slot/client-message identifier makes a lost-response retry return the existing capture rather than create another question or chat message. Ordinary chat keeps its existing WebSocket delivery; the question view polls a bounded snapshot every two seconds.
One vote per user per question is a database rule. This is the complete TalkQuestion model from that prerelease:
class TalkQuestion < ApplicationRecord
belongs_to :talk_slot
belongs_to :message
has_many :talk_votes, dependent: :destroy
validates :body, length: { in: 1..500 }
validates :client_message_id, length: { in: 1..100 }
def vote!(user)
talk_votes.create!(user: user)
rescue ActiveRecord::RecordNotUnique
talk_votes.find_by!(user_id: user.id)
end
endThe migration adds a unique index on (talk_question_id, user_id). Roundhouse’s generated native SQLite schema retains that index. vote! therefore handles a duplicate insert by returning the existing vote; a model validation alone would not enforce uniqueness between competing requests.
The snapshot includes at most the fifty earliest unanswered questions, ten recent answers and the pinned active question. Votes order the waiting questions within that window. Current active human room membership is checked on every pilot request; projection and moderation also require an administrator. This is room-scoped, without a public display token, schedule import or global question board.
The prerelease also self-hosts Instrument Sans under SIL OFL 1.1 in its font stylesheet. The native asset builder did not publish app/assets/fonts under /assets. Embedding the licensed font bytes in the stylesheet as a data URI avoided an absent font URL; the bytes served by the native binary were checked against the licensed source. This changes packaging, without adding a remote font request.
The exact published Linux binary was tested with synthetic Ask/retry, voting, moderator controls, default-off behavior and mobile/projection views. Later larger type and adaptive question fitting were compiled and tested locally, but remain unpublished. Those local visual checks cover a 1920 × 1080 stage; dense text at 1600 × 900, physical TV viewing distance and real keyboard behavior remain limitations. Neither test set establishes conference capacity or inherited stream and membership lifecycle correctness.
The following captures were taken on October 6 from the unpublished local Linux AMD64 candidate, with synthetic accounts and messages. Its binary SHA-256 is 3a370701e4170b4290249e8b30d373690c27a40e4eecbf4ad5d32ecfc32cd9a5. These are visual and serial behavior checks, not new production screenshots or a capacity test.
Telemetry measures dispatches, not a performance comparison
The authenticated /runtime dashboard makes a compiled build inspectable. The Python collector samples container metadata, cgroups, /proc and aggregate SQLite counts and sizes every five seconds, without collecting message bodies or email addresses. Prometheus scrapes every 15 seconds with fourteen-day and 2 GB retention limits.
RH_REQUEST_METRICS=1 enables Roundhouse’s native dispatcher events. The monotonic timer begins after acquiring a database connection lease and ends before writing the response to the socket. Its duration excludes that lease wait and response delivery. Byte fields count prepared response-body bytes or the file’s size, not bytes confirmed delivered over the wire. WebSocket upgrades record status 101; individual chat frames are outside this measurement.
Prometheus can estimate a five-minute p95 from the collected histogram:
1000 * histogram_quantile(
0.95,
sum by (le) (rate(campfire_http_request_duration_seconds_bucket[5m]))
)Preserving le retains the bucket boundaries; multiplying by 1000 converts seconds to milliseconds. histogram_quantile estimates within those buckets. The dashboard uses different calculations: request-count deltas between samples for its rate, and a nearest-rank p95 from at most 10,000 recent events within sixty seconds. Those values need not match the five-minute PromQL result.
The collector strips query strings, redacts unknown path segments and distinguishes missing or stale samples from zero. Completed dispatch events include assets, dashboard polling and WebSocket handshakes. They exclude requests rejected before the application and cannot establish end-to-end browser latency.
Revisions, checksums and the limits of the evidence
The October 4 deployment receipt pins all three components:
| Component | Revision |
|---|---|
| Campfire | 46838319 |
| Roundhouse | be39e428 |
| Spinel | ed603ed5 |
It records the executable as ELF, no Ruby interpreter in the inspected processes, and a deployed executable checksum matching the versioned release:
184e0e305d31efabb9dfb3b308f8a67c0bdf9175091cbb62615e92c1b651c050At 04:16 UTC the receipt observed 1.26% of one CPU, 42.5 MiB of cgroup memory and 75.2 MiB summed process RSS. It recorded 42,014 completed dispatch events since October 2 at 20:57 UTC, spanning multiple deployments. This is an uncontrolled live snapshot. Summed RSS can count shared pages repeatedly; cgroup memory uses container accounting. We have no controlled Rails-versus-native benchmark or capacity claim.
Verification follows the implementation boundary: Rails tests for source behavior, compiler regressions for emitted constructs, then the actual Linux binary for cookies, room navigation, message edits and delivery. The release validation separately records delegated identity and logo checks; it does not establish every attendee’s sign-in outcome or new screenshot coverage for that exact logo build.
The published 8c871c3 pilot, unpublished typography candidate and proposed cross-room features remain separate from this dated production release.
Read the pinned archive builder, Spinel packing implementation and Roundhouse runtime guide for the build mechanics.
